/*
 * Created by Olivier Chalouhi
 * Modified Apr 13, 2004 by Alon Rohter
 * Heavily modified Sep 2005 by Joseph Bridgewater
 * Copyright (C) Azureus Software, Inc, All Rights Reserved.
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

package org.gudy.azureus2.core3.peer.impl.control;

import java.net.Inet6Address;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.config.ParameterListener;
import org.gudy.azureus2.core3.disk.DiskManager;
import org.gudy.azureus2.core3.disk.DiskManager.GettingThere;
import org.gudy.azureus2.core3.disk.DiskManagerCheckRequest;
import org.gudy.azureus2.core3.disk.DiskManagerCheckRequestListener;
import org.gudy.azureus2.core3.disk.DiskManagerPiece;
import org.gudy.azureus2.core3.disk.DiskManagerReadRequest;
import org.gudy.azureus2.core3.disk.DiskManagerWriteRequest;
import org.gudy.azureus2.core3.disk.DiskManagerWriteRequestListener;
import org.gudy.azureus2.core3.internat.MessageText;
import org.gudy.azureus2.core3.ipfilter.BannedIp;
import org.gudy.azureus2.core3.ipfilter.IPFilterListener;
import org.gudy.azureus2.core3.ipfilter.IpFilter;
import org.gudy.azureus2.core3.ipfilter.IpFilterManager;
import org.gudy.azureus2.core3.ipfilter.IpFilterManagerFactory;
import org.gudy.azureus2.core3.logging.LogAlert;
import org.gudy.azureus2.core3.logging.LogEvent;
import org.gudy.azureus2.core3.logging.LogIDs;
import org.gudy.azureus2.core3.logging.LogRelation;
import org.gudy.azureus2.core3.logging.Logger;
import org.gudy.azureus2.core3.peer.PEPeer;
import org.gudy.azureus2.core3.peer.PEPeerManager;
import org.gudy.azureus2.core3.peer.PEPeerManagerAdapter;
import org.gudy.azureus2.core3.peer.PEPeerManagerListener;
import org.gudy.azureus2.core3.peer.PEPeerManagerStats;
import org.gudy.azureus2.core3.peer.PEPeerSource;
import org.gudy.azureus2.core3.peer.PEPeerStats;
import org.gudy.azureus2.core3.peer.PEPiece;
import org.gudy.azureus2.core3.peer.impl.PEPeerControl;
import org.gudy.azureus2.core3.peer.impl.PEPeerManagerStatsImpl;
import org.gudy.azureus2.core3.peer.impl.PEPeerStatsImpl;
import org.gudy.azureus2.core3.peer.impl.PEPeerTransport;
import org.gudy.azureus2.core3.peer.impl.PEPeerTransportFactory;
import org.gudy.azureus2.core3.peer.impl.PEPieceImpl;
import org.gudy.azureus2.core3.peer.impl.PEPieceWriteImpl;
import org.gudy.azureus2.core3.peer.util.PeerIdentityDataID;
import org.gudy.azureus2.core3.peer.util.PeerIdentityManager;
import org.gudy.azureus2.core3.peer.util.PeerUtils;
import org.gudy.azureus2.core3.torrent.TOTorrentException;
import org.gudy.azureus2.core3.tracker.client.TRTrackerAnnouncer;
import org.gudy.azureus2.core3.tracker.client.TRTrackerAnnouncerResponse;
import org.gudy.azureus2.core3.tracker.client.TRTrackerAnnouncerResponsePeer;
import org.gudy.azureus2.core3.tracker.client.TRTrackerScraperResponse;
import org.gudy.azureus2.core3.util.AEMonitor;
import org.gudy.azureus2.core3.util.AENetworkClassifier;
import org.gudy.azureus2.core3.util.AESemaphore;
import org.gudy.azureus2.core3.util.AEThread2;
import org.gudy.azureus2.core3.util.AddressUtils;
import org.gudy.azureus2.core3.util.Average;
import org.gudy.azureus2.core3.util.BrokenMd5Hasher;
import org.gudy.azureus2.core3.util.Constants;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.DirectByteBuffer;
import org.gudy.azureus2.core3.util.IndentWriter;
import org.gudy.azureus2.core3.util.RandomUtils;
import org.gudy.azureus2.core3.util.SystemTime;
import org.gudy.azureus2.core3.util.TimeFormatter;
import org.gudy.azureus2.plugins.download.DownloadAnnounceResultPeer;
import org.gudy.azureus2.plugins.network.Connection;
import org.gudy.azureus2.plugins.network.OutgoingMessageQueue;
import org.gudy.azureus2.plugins.peers.Peer;
import org.gudy.azureus2.plugins.peers.PeerDescriptor;

import com.aelitis.azureus.core.networkmanager.LimitedRateGroup;
import com.aelitis.azureus.core.networkmanager.admin.NetworkAdmin;
import com.aelitis.azureus.core.networkmanager.impl.tcp.TCPConnectionManager;
import com.aelitis.azureus.core.networkmanager.impl.tcp.TCPNetworkManager;
import com.aelitis.azureus.core.networkmanager.impl.udp.UDPNetworkManager;
import com.aelitis.azureus.core.peermanager.PeerManagerRegistration;
import com.aelitis.azureus.core.peermanager.control.PeerControlInstance;
import com.aelitis.azureus.core.peermanager.control.PeerControlScheduler;
import com.aelitis.azureus.core.peermanager.control.PeerControlSchedulerFactory;
import com.aelitis.azureus.core.peermanager.nat.PeerNATInitiator;
import com.aelitis.azureus.core.peermanager.nat.PeerNATTraversalAdapter;
import com.aelitis.azureus.core.peermanager.nat.PeerNATTraverser;
import com.aelitis.azureus.core.peermanager.peerdb.PeerDatabase;
import com.aelitis.azureus.core.peermanager.peerdb.PeerDatabaseFactory;
import com.aelitis.azureus.core.peermanager.peerdb.PeerExchangerItem;
import com.aelitis.azureus.core.peermanager.peerdb.PeerItem;
import com.aelitis.azureus.core.peermanager.peerdb.PeerItemFactory;
import com.aelitis.azureus.core.peermanager.piecepicker.PiecePicker;
import com.aelitis.azureus.core.peermanager.piecepicker.PiecePickerFactory;
import com.aelitis.azureus.core.peermanager.unchoker.Unchoker;
import com.aelitis.azureus.core.peermanager.unchoker.UnchokerFactory;
import com.aelitis.azureus.core.peermanager.unchoker.UnchokerUtil;
import com.aelitis.azureus.core.peermanager.uploadslots.UploadHelper;
import com.aelitis.azureus.core.peermanager.uploadslots.UploadSlotManager;
import com.aelitis.azureus.core.tracker.TrackerPeerSource;
import com.aelitis.azureus.core.tracker.TrackerPeerSourceAdapter;
import com.aelitis.azureus.core.util.FeatureAvailability;
import com.aelitis.azureus.core.util.bloom.BloomFilter;
import com.aelitis.azureus.core.util.bloom.BloomFilterFactory;

/**
 * manages all peer transports for a torrent
 * 
 * @author MjrTom 2005/Oct/08: Numerous changes for new piece-picking. Also a few optimizations and multi-thread cleanups 2006/Jan/02: refactoring
 *         piece picking related code
 */

@SuppressWarnings("serial")
public class PEPeerControlImpl extends LogRelation implements PEPeerControl, DiskManagerWriteRequestListener, PeerControlInstance, PeerNATInitiator, DiskManagerCheckRequestListener, IPFilterListener {
    private static final LogIDs LOGID = LogIDs.PEER;

    private static final boolean TEST_PERIODIC_SEEDING_SCAN_FAIL_HANDLING = false;

    static {
        if (TEST_PERIODIC_SEEDING_SCAN_FAIL_HANDLING) {
            Debug.out("**** test periodic scan failure enabled ****");
        }
    }

    private static final int WARNINGS_LIMIT = 2;

    private static final int CHECK_REASON_DOWNLOADED = 1;
    private static final int CHECK_REASON_COMPLETE = 2;
    private static final int CHECK_REASON_SCAN = 3;
    private static final int CHECK_REASON_SEEDING_CHECK = 4;
    private static final int CHECK_REASON_BAD_PIECE_CHECK = 5;

    private static final int SEED_CHECK_WAIT_MARKER = 65526;

    // config

    private static boolean disconnect_seeds_when_seeding;
    private static boolean enable_seeding_piece_rechecks;
    private static int stalled_piece_timeout;
    private static boolean fast_unchoke_new_peers;
    private static float ban_peer_discard_ratio;
    private static int ban_peer_discard_min_kb;
    private static boolean udp_fallback_for_failed_connection;
    private static boolean udp_fallback_for_dropped_connection;
    private static boolean udp_probe_enabled;
    private static boolean hide_a_piece;
    private static boolean prefer_udp_default;

    static {

        COConfigurationManager.addAndFireParameterListeners(new String[] { "Disconnect Seed", "Seeding Piece Check Recheck Enable",
                "peercontrol.stalled.piece.write.timeout", "Peer.Fast.Initial.Unchoke.Enabled", "Ip Filter Ban Discard Ratio",
                "Ip Filter Ban Discard Min KB", "peercontrol.udp.fallback.connect.fail", "peercontrol.udp.fallback.connect.drop",
                "peercontrol.udp.probe.enable", "peercontrol.hide.piece", "peercontrol.hide.piece.ds", "peercontrol.prefer.udp", },
                new ParameterListener() {
                    public void parameterChanged(String name) {
                        disconnect_seeds_when_seeding = COConfigurationManager.getBooleanParameter("Disconnect Seed");
                        enable_seeding_piece_rechecks = COConfigurationManager.getBooleanParameter("Seeding Piece Check Recheck Enable");
                        stalled_piece_timeout = COConfigurationManager.getIntParameter("peercontrol.stalled.piece.write.timeout", 60 * 1000);
                        fast_unchoke_new_peers = COConfigurationManager.getBooleanParameter("Peer.Fast.Initial.Unchoke.Enabled");
                        ban_peer_discard_ratio = COConfigurationManager.getFloatParameter("Ip Filter Ban Discard Ratio");
                        ban_peer_discard_min_kb = COConfigurationManager.getIntParameter("Ip Filter Ban Discard Min KB");
                        udp_fallback_for_failed_connection = COConfigurationManager.getBooleanParameter("peercontrol.udp.fallback.connect.fail");
                        udp_fallback_for_dropped_connection = COConfigurationManager.getBooleanParameter("peercontrol.udp.fallback.connect.drop");
                        udp_probe_enabled = COConfigurationManager.getBooleanParameter("peercontrol.udp.probe.enable");
                        hide_a_piece = COConfigurationManager.getBooleanParameter("peercontrol.hide.piece");
                        boolean hide_a_piece_ds = COConfigurationManager.getBooleanParameter("peercontrol.hide.piece.ds");

                        if (hide_a_piece && !hide_a_piece_ds) {

                            disconnect_seeds_when_seeding = false;
                        }

                        prefer_udp_default = COConfigurationManager.getBooleanParameter("peercontrol.prefer.udp");
                    }
                });
    }

    private static IpFilter ip_filter = IpFilterManagerFactory.getSingleton().getIPFilter();

    private volatile boolean is_running = false;
    private volatile boolean is_destroyed = false;

    private volatile ArrayList<PEPeer> peer_transports_cow = new ArrayList<PEPeer>(); // Copy on write!
    private final AEMonitor peer_transports_mon = new AEMonitor("PEPeerControl:PT");

    protected final PEPeerManagerAdapter adapter;
    private final DiskManager disk_mgr;
    private final DiskManagerPiece[] dm_pieces;

    private final boolean is_private_torrent;

    private PEPeerManager.StatsReceiver stats_receiver;

    private final PiecePicker piecePicker;
    private long lastNeededUndonePieceChange;

    /** literally seeding as in 100% torrent complete */
    private boolean seeding_mode;
    private boolean restart_initiated;

    private final int _nbPieces; // how many pieces in the torrent
    private PEPieceImpl[] pePieces; // pieces that are currently in progress
    private int nbPiecesActive; // how many pieces are currently in progress

    private int nbPeersSnubbed;

    private PeerIdentityDataID _hash;
    private final byte[] _myPeerId;
    private PEPeerManagerStatsImpl _stats;

    // private final TRTrackerAnnouncer _tracker;
    // private int _maxUploads;
    private int stats_tick_count;
    private int _seeds, _peers, _remotesTCPNoLan, _remotesUDPNoLan, _remotesUTPNoLan;
    private int _tcpPendingConnections, _tcpConnectingConnections;
    private long last_remote_time;
    private long _timeStarted;
    private long _timeStarted_mono;
    private long _timeStartedSeeding = -1;
    private long _timeStartedSeeding_mono = -1;
    private long _timeFinished;
    private Average _averageReceptionSpeed;

    private long mainloop_loop_count;

    private static final int MAINLOOP_ONE_SECOND_INTERVAL = 1000 / PeerControlScheduler.SCHEDULE_PERIOD_MILLIS;
    private static final int MAINLOOP_FIVE_SECOND_INTERVAL = MAINLOOP_ONE_SECOND_INTERVAL * 5;
    private static final int MAINLOOP_TEN_SECOND_INTERVAL = MAINLOOP_ONE_SECOND_INTERVAL * 10;
    private static final int MAINLOOP_TWENTY_SECOND_INTERVAL = MAINLOOP_ONE_SECOND_INTERVAL * 20;
    private static final int MAINLOOP_THIRTY_SECOND_INTERVAL = MAINLOOP_ONE_SECOND_INTERVAL * 30;
    private static final int MAINLOOP_SIXTY_SECOND_INTERVAL = MAINLOOP_ONE_SECOND_INTERVAL * 60;
    private static final int MAINLOOP_TEN_MINUTE_INTERVAL = MAINLOOP_SIXTY_SECOND_INTERVAL * 10;

    private volatile ArrayList<PEPeerManagerListener> peer_manager_listeners_cow = new ArrayList<PEPeerManagerListener>(); // copy on write

    private final List<Object[]> piece_check_result_list = new ArrayList<Object[]>();
    private final AEMonitor piece_check_result_list_mon = new AEMonitor("PEPeerControl:PCRL");

    private boolean superSeedMode;
    private int superSeedModeCurrentPiece;
    private int superSeedModeNumberOfAnnounces;
    private SuperSeedPiece[] superSeedPieces;

    private int hidden_piece;

    private final AEMonitor this_mon = new AEMonitor("PEPeerControl");

    private long ip_filter_last_update_time;

    private Map<Object, Object> user_data;

    private Unchoker unchoker;

    private List<Object[]> external_rate_limiters_cow;

    private int bytes_queued_for_upload;
    private int connections_with_queued_data;
    private int connections_with_queued_data_blocked;
    private int connections_unchoked;

    private List<PEPeer> sweepList = Collections.emptyList();
    private int nextPEXSweepIndex = 0;

    private final UploadHelper upload_helper = new UploadHelper() {
        public int getPriority() {
            return UploadHelper.PRIORITY_NORMAL; // TODO also must call UploadSlotManager.getSingleton().updateHelper( upload_helper ); on priority
                                                 // change
        }

        public ArrayList<PEPeer> getAllPeers() {
            return (peer_transports_cow);
        }

        public boolean isSeeding() {
            return seeding_mode;
        }
    };

    private PeerDatabase peer_database = PeerDatabaseFactory.createPeerDatabase();

    private int bad_piece_reported = -1;

    private int next_rescan_piece = -1;
    private long rescan_piece_time = -1;

    private long last_eta;
    private long last_eta_smoothed;
    private long last_eta_calculation;

    private static final int MAX_UDP_CONNECTIONS = 16;

    private static final int PENDING_NAT_TRAVERSAL_MAX = 32;
    private static final int MAX_UDP_TRAVERSAL_COUNT = 3;

    private static final String PEER_NAT_TRAVERSE_DONE_KEY = PEPeerControlImpl.class.getName() + "::nat_trav_done";

    private Map<String, PEPeerTransport> pending_nat_traversals =
            new LinkedHashMap<String, PEPeerTransport>(PENDING_NAT_TRAVERSAL_MAX, 0.75f, true) {
                protected boolean removeEldestEntry(Map.Entry<String, PEPeerTransport> eldest) {
                    return size() > PENDING_NAT_TRAVERSAL_MAX;
                }
            };

    private int udp_traversal_count;

    private static final int UDP_RECONNECT_MAX = 16;

    private Map<String, PEPeerTransport> udp_reconnects = new LinkedHashMap<String, PEPeerTransport>(UDP_RECONNECT_MAX, 0.75f, true) {
        protected boolean removeEldestEntry(Map.Entry<String, PEPeerTransport> eldest) {
            return size() > UDP_RECONNECT_MAX;
        }
    };

    private static final int UDP_RECONNECT_MIN_MILLIS = 10 * 1000;
    private long last_udp_reconnect;

    private boolean prefer_udp;

    private static final int PREFER_UDP_BLOOM_SIZE = 10000;
    private volatile BloomFilter prefer_udp_bloom;

    private final LimitedRateGroup upload_limited_rate_group = new LimitedRateGroup() {
        public String getName() {
            return ("per_dl_up: " + getDisplayName());
        }

        public int getRateLimitBytesPerSecond() {
            return adapter.getUploadRateLimitBytesPerSecond();
        }

        public void updateBytesUsed(int used) {
        }
    };

    private final LimitedRateGroup download_limited_rate_group = new LimitedRateGroup() {
        public String getName() {
            return ("per_dl_down: " + getDisplayName());
        }

        public int getRateLimitBytesPerSecond() {
            return adapter.getDownloadRateLimitBytesPerSecond();
        }

        public void updateBytesUsed(int used) {
        }
    };

    private final int partition_id;

    private final boolean is_metadata_download;
    private int metadata_infodict_size;

    private GettingThere finish_in_progress;

    private long last_seed_disconnect_time;

    private BloomFilter naughty_fast_extension_bloom = BloomFilterFactory.createRotating(BloomFilterFactory.createAddRemove4Bit(2000), 2);

    public PEPeerControlImpl(byte[] _peer_id, PEPeerManagerAdapter _adapter, DiskManager _diskManager, int _partition_id) {
        _myPeerId = _peer_id;
        adapter = _adapter;
        disk_mgr = _diskManager;
        partition_id = _partition_id;

        boolean is_private = false;

        try {
            is_private = disk_mgr.getTorrent().getPrivate();

        } catch (Throwable e) {

            Debug.out(e);
        }

        is_private_torrent = is_private;

        is_metadata_download = adapter.isMetadataDownload();

        if (!is_metadata_download) {
            metadata_infodict_size = adapter.getTorrentInfoDictSize();
        }

        _nbPieces = disk_mgr.getNbPieces();
        dm_pieces = disk_mgr.getPieces();

        pePieces = new PEPieceImpl[_nbPieces];

        hidden_piece = hide_a_piece ? ((int) (Math.abs(adapter.getRandomSeed()) % _nbPieces)) : -1;

        /*
         * if ( hidden_piece >= 0 ){ System.out.println( "Hidden piece for " + getDisplayName() + " = " + hidden_piece ); }
         */

        piecePicker = PiecePickerFactory.create(this);

        ip_filter.addListener(this);
    }

    public void start() {
        // This torrent Hash
        try {

            _hash = PeerIdentityManager.createDataID(disk_mgr.getTorrent().getHash());

        } catch (TOTorrentException e) {

            // this should never happen
            Debug.printStackTrace(e);

            _hash = PeerIdentityManager.createDataID(new byte[20]);
        }

        // the recovered active pieces
        for (int i = 0; i < _nbPieces; i++) {
            final DiskManagerPiece dmPiece = dm_pieces[i];
            if (!dmPiece.isDone() && dmPiece.getNbWritten() > 0) {
                addPiece(new PEPieceImpl(this, dmPiece, 0), i, true, null);
            }
        }

        // The peer connections
        peer_transports_cow = new ArrayList();

        // BtManager is threaded, this variable represents the
        // current loop iteration. It's used by some components only called
        // at some specific times.
        mainloop_loop_count = 0;

        // The current tracker state
        // this could be start or update

        _averageReceptionSpeed = Average.getInstance(1000, 30);

        // the stats
        _stats = new PEPeerManagerStatsImpl(this);

        superSeedMode = (COConfigurationManager.getBooleanParameter("Use Super Seeding") && this.getRemaining() == 0);

        superSeedModeCurrentPiece = 0;

        if (superSeedMode) {
            initialiseSuperSeedMode();
        }

        // initial check on finished state - future checks are driven by piece check results

        // Moved out of mainLoop() so that it runs immediately, possibly changing
        // the state to seeding.

        checkFinished(true);

        UploadSlotManager.getSingleton().registerHelper(upload_helper);

        lastNeededUndonePieceChange = Long.MIN_VALUE;
        _timeStarted = SystemTime.getCurrentTime();
        _timeStarted_mono = SystemTime.getMonotonousTime();

        is_running = true;

        // activate after marked as running as we may synchronously add connections here due to pending activations

        PeerManagerRegistration reg = adapter.getPeerManagerRegistration();

        if (reg != null) {

            reg.activate(this);
        }

        PeerNATTraverser.getSingleton().register(this);

        PeerControlSchedulerFactory.getSingleton(partition_id).register(this);
    }

    public void stopAll() {
        is_running = false;

        UploadSlotManager.getSingleton().deregisterHelper(upload_helper);

        PeerControlSchedulerFactory.getSingleton(partition_id).unregister(this);

        PeerNATTraverser.getSingleton().unregister(this);

        // remove legacy controller activation

        PeerManagerRegistration reg = adapter.getPeerManagerRegistration();

        if (reg != null) {

            reg.deactivate();
        }

        closeAndRemoveAllPeers("download stopped", false);

        // clear pieces
        for (int i = 0; i < _nbPieces; i++) {
            if (pePieces[i] != null)
                removePiece(pePieces[i], i);
        }

        // 5. Remove listeners

        ip_filter.removeListener(this);

        piecePicker.destroy();

        final ArrayList<PEPeerManagerListener> peer_manager_listeners = peer_manager_listeners_cow;

        for (int i = 0; i < peer_manager_listeners.size(); i++) {

            ((PEPeerManagerListener) peer_manager_listeners.get(i)).destroyed();
        }

        sweepList = Collections.emptyList();

        pending_nat_traversals.clear();

        udp_reconnects.clear();

        is_destroyed = true;
    }

    public int getPartitionID() {
        return (partition_id);
    }

    public boolean isDestroyed() {
        return (is_destroyed);
    }

    public DiskManager getDiskManager() {
        return disk_mgr;
    }

    public PiecePicker getPiecePicker() {
        return piecePicker;
    }

    public PEPeerManagerAdapter getAdapter() {
        return (adapter);
    }

    public String getDisplayName() {
        return (adapter.getDisplayName());
    }

    public String getName() {
        return (getDisplayName());
    }

    public void schedule() {
        if (finish_in_progress != null) {

            // System.out.println( "Finish in prog" );

            if (finish_in_progress.hasGotThere()) {

                finish_in_progress = null;

                // System.out.println( "Finished" );
            } else {

                return;
            }
        }

        try {
            // first off update the stats so they can be used by subsequent steps

            updateStats();

            updateTrackerAnnounceInterval();

            doConnectionChecks();

            processPieceChecks();

            if (finish_in_progress != null) {

                // get off the scheduler thread while potentially long running operations complete

                return;
            }

            // note that seeding_mode -> torrent totally downloaded, not just non-dnd files
            // complete, so there is no change of a new piece appearing done by a means such as
            // background periodic file rescans

            if (!seeding_mode) {

                checkCompletedPieces(); // check to see if we've completed anything else
            }

            checkBadPieces();

            checkInterested(); // see if need to recheck Interested on all peers

            piecePicker.updateAvailability();

            checkCompletionState(); // pick up changes in completion caused by dnd file changes

            if (finish_in_progress != null) {

                // get off the scheduler thread while potentially long running operations complete

                return;
            }

            checkSeeds();

            if (!seeding_mode) {
                // if we're not finished

                checkRequests();

                piecePicker.allocateRequests();

                checkRescan();
                checkSpeedAndReserved();

                check99PercentBug();
            }

            updatePeersInSuperSeedMode();
            doUnchokes();

        } catch (Throwable e) {

            Debug.printStackTrace(e);
        }
        mainloop_loop_count++;
    }

    /**
     * A private method that does analysis of the result sent by the tracker. It will mainly open new connections with peers provided and set the
     * timeToWait variable according to the tracker response.
     * 
     * @param tracker_response
     */

    private void analyseTrackerResponse(TRTrackerAnnouncerResponse tracker_response) {
        // tracker_response.print();
        final TRTrackerAnnouncerResponsePeer[] peers = tracker_response.getPeers();

        if (peers != null) {
            addPeersFromTracker(tracker_response.getPeers());
        }

        final Map extensions = tracker_response.getExtensions();

        if (extensions != null) {
            addExtendedPeersFromTracker(extensions);
        }
    }

    public void processTrackerResponse(TRTrackerAnnouncerResponse response) {
        // only process new peers if we're still running
        if (is_running) {
            analyseTrackerResponse(response);
        }
    }

    private void addExtendedPeersFromTracker(Map extensions) {
        final Map protocols = (Map) extensions.get("protocols");

        if (protocols != null) {

            System.out.println("PEPeerControl: tracker response contained protocol extensions");

            final Iterator protocol_it = protocols.keySet().iterator();

            while (protocol_it.hasNext()) {

                final String protocol_name = (String) protocol_it.next();

                final Map protocol = (Map) protocols.get(protocol_name);

                final List transports = PEPeerTransportFactory.createExtendedTransports(this, protocol_name, protocol);

                for (int i = 0; i < transports.size(); i++) {

                    final PEPeer transport = (PEPeer) transports.get(i);

                    addPeer(transport);
                }
            }
        }
    }

    public List<PEPeer> getPeers() {
        return (peer_transports_cow);
    }

    public List<PEPeer> getPeers(String address) {
        List<PEPeer> result = new ArrayList<PEPeer>();

        Iterator<PEPeer> it = peer_transports_cow.iterator();

        while (it.hasNext()) {

            PEPeerTransport peer = (PEPeerTransport) it.next();

            if (peer.getIp().equals(address)) {

                result.add(peer);
            }
        }

        return (result);
    }

    public int getPendingPeerCount() {
        return (peer_database.getDiscoveredPeerCount());
    }

    public PeerDescriptor[] getPendingPeers() {
        return ((PeerDescriptor[]) peer_database.getDiscoveredPeers());
    }

    public PeerDescriptor[] getPendingPeers(String address) {
        return ((PeerDescriptor[]) peer_database.getDiscoveredPeers(address));
    }

    public void addPeer(PEPeer _transport) {
        if (!(_transport instanceof PEPeerTransport)) {

            throw (new RuntimeException("invalid class"));
        }

        final PEPeerTransport transport = (PEPeerTransport) _transport;

        if (!ip_filter.isInRange(transport.getIp(), getDisplayName(), getTorrentHash())) {

            final ArrayList peer_transports = peer_transports_cow;

            if (!peer_transports.contains(transport)) {

                addToPeerTransports(transport);

                transport.start();

            } else {
                Debug.out("addPeer():: peer_transports.contains(transport): SHOULD NEVER HAPPEN !");
                transport.closeConnection("already connected");
            }
        } else {

            transport.closeConnection("IP address blocked by filters");
        }
    }

    protected byte[] getTorrentHash() {
        try {
            return (disk_mgr.getTorrent().getHash());

        } catch (Throwable e) {

            return (null);
        }
    }

    public void removePeer(PEPeer _transport) {
        removePeer(_transport, "remove peer");
    }

    public void removePeer(PEPeer _transport, String reason) {
        if (!(_transport instanceof PEPeerTransport)) {

            throw (new RuntimeException("invalid class"));
        }

        PEPeerTransport transport = (PEPeerTransport) _transport;

        closeAndRemovePeer(transport, reason, true);
    }

    private void closeAndRemovePeer(PEPeerTransport peer, String reason, boolean log_if_not_found) {
        boolean removed = false;

        // copy-on-write semantics
        try {
            peer_transports_mon.enter();

            if (peer_transports_cow.contains(peer)) {

                final ArrayList new_peer_transports = new ArrayList(peer_transports_cow);

                new_peer_transports.remove(peer);
                peer_transports_cow = new_peer_transports;
                removed = true;
            }
        } finally {
            peer_transports_mon.exit();
        }

        if (removed) {
            peer.closeConnection(reason);
            peerRemoved(peer); // notify listeners
        } else {
            if (log_if_not_found) {
                // we know this happens due to timing issues... Debug.out( "closeAndRemovePeer(): peer not removed" );
            }
        }
    }

    private void closeAndRemoveAllPeers(String reason, boolean reconnect) {
        ArrayList peer_transports;

        try {
            peer_transports_mon.enter();

            peer_transports = peer_transports_cow;

            peer_transports_cow = new ArrayList(0);
        } finally {
            peer_transports_mon.exit();
        }

        for (int i = 0; i < peer_transports.size(); i++) {
            final PEPeerTransport peer = (PEPeerTransport) peer_transports.get(i);

            try {

                peer.closeConnection(reason);

            } catch (Throwable e) {

                // if something goes wrong with the close process (there's a bug in there somewhere whereby
                // we occasionally get NPEs then we want to make sure we carry on and close the rest

                Debug.printStackTrace(e);
            }

            try {
                peerRemoved(peer); // notify listeners

            } catch (Throwable e) {

                Debug.printStackTrace(e);
            }
        }

        if (reconnect) {
            for (int i = 0; i < peer_transports.size(); i++) {
                final PEPeerTransport peer = (PEPeerTransport) peer_transports.get(i);

                PEPeerTransport reconnected_peer = peer.reconnect(false, false);
            }
        }
    }

    public void addPeer(String ip_address, int tcp_port, int udp_port, boolean use_crypto, Map user_data) {
        final byte type = use_crypto ? PeerItemFactory.HANDSHAKE_TYPE_CRYPTO : PeerItemFactory.HANDSHAKE_TYPE_PLAIN;
        final PeerItem peer_item =
                PeerItemFactory.createPeerItem(ip_address, tcp_port, PeerItem.convertSourceID(PEPeerSource.PS_PLUGIN), type, udp_port,
                        PeerItemFactory.CRYPTO_LEVEL_1, 0);

        byte crypto_level = PeerItemFactory.CRYPTO_LEVEL_1;

        if (!isAlreadyConnected(peer_item)) {

            String fail_reason;

            boolean tcp_ok = TCPNetworkManager.TCP_OUTGOING_ENABLED && tcp_port > 0;
            boolean udp_ok = UDPNetworkManager.UDP_OUTGOING_ENABLED && udp_port > 0;

            if (tcp_ok && !((prefer_udp || prefer_udp_default) && udp_ok)) {

                fail_reason =
                        makeNewOutgoingConnection(PEPeerSource.PS_PLUGIN, ip_address, tcp_port, udp_port, true, use_crypto, crypto_level, user_data); // directly
                                                                                                                                                      // inject
                                                                                                                                                      // the
                                                                                                                                                      // the
                                                                                                                                                      // imported
                                                                                                                                                      // peer

            } else if (udp_ok) {

                fail_reason =
                        makeNewOutgoingConnection(PEPeerSource.PS_PLUGIN, ip_address, tcp_port, udp_port, false, use_crypto, crypto_level,
                                user_data); // directly inject the the imported peer

            } else {

                fail_reason = "No usable protocol";
            }

            if (fail_reason != null)
                Debug.out("Injected peer " + ip_address + ":" + tcp_port + " was not added - " + fail_reason);
        }
    }

    public void peerDiscovered(String peer_source, String ip_address, int tcp_port, int udp_port, boolean use_crypto) {
        if (peer_database != null) {

            final ArrayList<PEPeer> peer_transports = peer_transports_cow;

            for (int x = 0; x < peer_transports.size(); x++) {

                PEPeer transport = peer_transports.get(x);

                // allow loopback connects for co-located proxy-based connections and testing

                if (ip_address.equals(transport.getIp())) {

                    boolean same_allowed = COConfigurationManager.getBooleanParameter("Allow Same IP Peers") ||

                    transport.getIp().equals("127.0.0.1");

                    if (!same_allowed || tcp_port == transport.getPort()) {

                        return;
                    }
                }
            }

            byte type = use_crypto ? PeerItemFactory.HANDSHAKE_TYPE_CRYPTO : PeerItemFactory.HANDSHAKE_TYPE_PLAIN;

            PeerItem item =
                    PeerItemFactory.createPeerItem(ip_address, tcp_port, PeerItem.convertSourceID(peer_source), type, udp_port,
                            PeerItemFactory.CRYPTO_LEVEL_1, 0);

            peerDiscovered(null, item);

            peer_database.addDiscoveredPeer(item);
        }
    }

    private void addPeersFromTracker(TRTrackerAnnouncerResponsePeer[] peers) {

        for (int i = 0; i < peers.length; i++) {
            final TRTrackerAnnouncerResponsePeer peer = peers[i];

            final ArrayList peer_transports = peer_transports_cow;

            boolean already_connected = false;

            for (int x = 0; x < peer_transports.size(); x++) {
                final PEPeerTransport transport = (PEPeerTransport) peer_transports.get(x);

                // allow loopback connects for co-located proxy-based connections and testing

                if (peer.getAddress().equals(transport.getIp())) {

                    final boolean same_allowed = COConfigurationManager.getBooleanParameter("Allow Same IP Peers") ||

                    transport.getIp().equals("127.0.0.1");

                    if (!same_allowed || peer.getPort() == transport.getPort()) {
                        already_connected = true;
                        break;
                    }
                }
            }

            if (already_connected)
                continue;

            if (peer_database != null) {

                byte type =
                        peer.getProtocol() == DownloadAnnounceResultPeer.PROTOCOL_CRYPT ? PeerItemFactory.HANDSHAKE_TYPE_CRYPTO
                                : PeerItemFactory.HANDSHAKE_TYPE_PLAIN;

                byte crypto_level =
                        peer.getAZVersion() < TRTrackerAnnouncer.AZ_TRACKER_VERSION_3 ? PeerItemFactory.CRYPTO_LEVEL_1
                                : PeerItemFactory.CRYPTO_LEVEL_2;

                PeerItem item =
                        PeerItemFactory.createPeerItem(peer.getAddress(), peer.getPort(), PeerItem.convertSourceID(peer.getSource()), type, peer
                                .getUDPPort(), crypto_level, peer.getUploadSpeed());

                peerDiscovered(null, item);

                peer_database.addDiscoveredPeer(item);
            }

            int http_port = peer.getHTTPPort();

            if (http_port != 0 && !seeding_mode) {

                adapter.addHTTPSeed(peer.getAddress(), http_port);
            }
        }
    }

    /**
     * Request a new outgoing peer connection.
     * 
     * @param address
     *            ip of remote peer
     * @param port
     *            remote peer listen port
     * @return null if the connection was added to the transport list, reason if rejected
     */

    private String makeNewOutgoingConnection(String peer_source, String address, int tcp_port, int udp_port, boolean use_tcp,
            boolean require_crypto, byte crypto_level, Map user_data) {
        // make sure this connection isn't filtered

        if (ip_filter.isInRange(address, getDisplayName(), getTorrentHash())) {

            return ("IPFilter block");
        }

        String net_cat = AENetworkClassifier.categoriseAddress(address);

        if (!adapter.isNetworkEnabled(net_cat)) {

            return ("Network '" + net_cat + "' is not enabled");
        }

        // make sure we need a new connection

        final int needed = getMaxNewConnectionsAllowed();

        boolean is_priority_connection = false;

        if (user_data != null) {

            Boolean pc = (Boolean) user_data.get(Peer.PR_PRIORITY_CONNECTION);

            if (pc != null && pc.booleanValue()) {

                is_priority_connection = true;
            }
        }

        if (needed == 0) {

            if (peer_source != PEPeerSource.PS_PLUGIN
                    || !doOptimisticDisconnect(AddressUtils.isLANLocalAddress(address) != AddressUtils.LAN_LOCAL_NO, is_priority_connection)) {

                return ("Too many connections");
            }
        }

        // make sure not already connected to the same IP address; allow loopback connects for co-located proxy-based connections and testing

        final boolean same_allowed = COConfigurationManager.getBooleanParameter("Allow Same IP Peers") || address.equals("127.0.0.1");

        if (!same_allowed && PeerIdentityManager.containsIPAddress(_hash, address)) {

            return ("Already connected to IP");
        }

        if (PeerUtils.ignorePeerPort(tcp_port)) {
            if (Logger.isEnabled())
                Logger.log(new LogEvent(disk_mgr.getTorrent(), LOGID, "Skipping connect with " + address + ":" + tcp_port
                        + " as peer port is in ignore list."));

            return ("TCP port '" + tcp_port + "' is in ignore list");
        }

        // start the connection

        PEPeerTransport real =
                PEPeerTransportFactory.createTransport(this, peer_source, address, tcp_port, udp_port, use_tcp, require_crypto, crypto_level,
                        user_data);

        addToPeerTransports(real);

        return null;
    }

    /**
     * A private method that checks if PEPieces being downloaded are finished If all blocks from a PEPiece are written to disk, this method will queue
     * the piece for hash check. Elsewhere, if it passes sha-1 check, it will be marked as downloaded, otherwise, it will unmark it as fully
     * downloaded, so blocks can be retreived again.
     */
    private void checkCompletedPieces() {
        if ((mainloop_loop_count % MAINLOOP_ONE_SECOND_INTERVAL) != 0)
            return;

        // for every piece
        for (int i = 0; i < _nbPieces; i++) {
            final DiskManagerPiece dmPiece = dm_pieces[i];
            // if piece is completly written, not already checking, and not Done
            if (dmPiece.isNeedsCheck()) {
                // check the piece from the disk
                dmPiece.setChecking();

                DiskManagerCheckRequest req = disk_mgr.createCheckRequest(i, new Integer(CHECK_REASON_DOWNLOADED));

                req.setAdHoc(false);

                disk_mgr.enqueueCheckRequest(req, this);
            }
        }
    }

    /**
     * Checks given piece to see if it's active but empty, and if so deactivates it.
     * 
     * @param pieceNumber
     *            to check
     * @return true if the piece was removed and is no longer active (pePiece ==null)
     */
    private boolean checkEmptyPiece(final int pieceNumber) {
        if (piecePicker.isInEndGameMode()) {

            return false; // be sure to not remove pieces in EGM
        }

        final PEPiece pePiece = pePieces[pieceNumber];
        final DiskManagerPiece dmPiece = dm_pieces[pieceNumber];

        if (pePiece == null || pePiece.isRequested())
            return false;

        if (dmPiece.getNbWritten() > 0 || pePiece.getNbUnrequested() < pePiece.getNbBlocks() || pePiece.getReservedBy() != null)
            return false;

        // reset in case dmpiece is in some skanky state

        pePiece.reset();

        removePiece(pePiece, pieceNumber);
        return true;
    }

    /**
     * Check if a piece's Speed is too fast for it to be getting new data and if a reserved pieced failed to get data within 120 seconds
     */
    private void checkSpeedAndReserved() {
        // only check every 5 seconds
        if (mainloop_loop_count % MAINLOOP_FIVE_SECOND_INTERVAL != 0)
            return;

        final int nbPieces = _nbPieces;
        final PEPieceImpl[] pieces = pePieces;
        // for every piece
        for (int i = 0; i < nbPieces; i++) {
            // placed before null-check in case it really removes a piece
            checkEmptyPiece(i);

            final PEPieceImpl pePiece = pieces[i];
            // these checks are only against pieces being downloaded
            // yet needing requests still/again
            if (pePiece != null) {
                final long timeSinceActivity = pePiece.getTimeSinceLastActivity() / 1000;

                int pieceSpeed = pePiece.getSpeed();
                // block write speed slower than piece speed
                if (pieceSpeed > 0 && timeSinceActivity * pieceSpeed * 0.25 > DiskManager.BLOCK_SIZE / 1024) {
                    if (pePiece.getNbUnrequested() > 2)
                        pePiece.setSpeed(pieceSpeed - 1);
                    else
                        pePiece.setSpeed(0);
                }

                if (timeSinceActivity > 120) {
                    pePiece.setSpeed(0);
                    // has reserved piece gone stagnant?
                    final String reservingPeer = pePiece.getReservedBy();
                    if (reservingPeer != null) {
                        final PEPeerTransport pt = getTransportFromAddress(reservingPeer);
                        // Peer is too slow; Ban them and unallocate the piece
                        // but, banning is no good for peer types that get pieces reserved
                        // to them for other reasons, such as special seed peers
                        if (needsMD5CheckOnCompletion(i))
                            badPeerDetected(reservingPeer, i);
                        else if (pt != null)
                            closeAndRemovePeer(pt, "Reserved piece data timeout; 120 seconds", true);

                        pePiece.setReservedBy(null);
                    }

                    if (!piecePicker.isInEndGameMode()) {
                        pePiece.checkRequests();
                    }

                    checkEmptyPiece(i);
                }

            }
        }
    }

    private void check99PercentBug() {
        // there's a bug whereby pieces are left downloaded but never written. might have been fixed by
        // changes to the "write result" logic, however as a stop gap I'm adding code to scan for such
        // stuck pieces and reset them

        if (mainloop_loop_count % MAINLOOP_SIXTY_SECOND_INTERVAL == 0) {

            long now = SystemTime.getCurrentTime();

            for (int i = 0; i < pePieces.length; i++) {

                PEPiece pe_piece = pePieces[i];

                if (pe_piece != null) {

                    DiskManagerPiece dm_piece = dm_pieces[i];

                    if (!dm_piece.isDone()) {

                        if (pe_piece.isDownloaded()) {

                            if (now - pe_piece.getLastDownloadTime(now) > stalled_piece_timeout) {

                                // people with *very* slow disk writes can trigger this (I've been talking to a user
                                // with a SAN that has .5 second write latencies when checking a file at the same time
                                // this means that when dowloading > 32K/sec things start backing up). Eventually the
                                // write controller will start blocking the network thread to prevent unlimited
                                // queueing but until that time we need to handle this situation slightly better)

                                // if there are any outstanding requests for this piece then leave it alone

                                if (!(disk_mgr.hasOutstandingWriteRequestForPiece(i) || disk_mgr.hasOutstandingReadRequestForPiece(i) || disk_mgr
                                        .hasOutstandingCheckRequestForPiece(i))) {

                                    Debug.out("Fully downloaded piece stalled pending write, resetting p_piece " + i);

                                    pe_piece.reset();
                                }
                            }
                        }
                    }
                }

            }

            if (hidden_piece >= 0) {

                int hp_avail = piecePicker.getAvailability(hidden_piece);

                if (hp_avail < (dm_pieces[hidden_piece].isDone() ? 2 : 1)) {

                    int[] avails = piecePicker.getAvailability();

                    int num = 0;

                    for (int i = 0; i < avails.length; i++) {

                        if (avails[i] > 0 && !dm_pieces[i].isDone() && pePieces[i] == null) {

                            num++;
                        }
                    }

                    if (num > 0) {

                        num = RandomUtils.nextInt(num);

                        int backup = -1;

                        for (int i = 0; i < avails.length; i++) {

                            if (avails[i] > 0 && !dm_pieces[i].isDone() && pePieces[i] == null) {

                                if (backup == -1) {

                                    backup = i;
                                }

                                if (num == 0) {

                                    hidden_piece = i;

                                    backup = -1;

                                    break;
                                }

                                num--;
                            }
                        }

                        if (backup != -1) {

                            hidden_piece = backup;
                        }
                    }
                }
            }
        }
    }

    private void checkInterested() {
        if ((mainloop_loop_count % MAINLOOP_ONE_SECOND_INTERVAL) != 0) {
            return;
        }

        if (lastNeededUndonePieceChange >= piecePicker.getNeededUndonePieceChange())
            return;

        lastNeededUndonePieceChange = piecePicker.getNeededUndonePieceChange();

        final ArrayList peer_transports = peer_transports_cow;
        int cntPeersSnubbed = 0; // recount # snubbed peers while we're at it
        for (int i = 0; i < peer_transports.size(); i++) {
            final PEPeerTransport peer = (PEPeerTransport) peer_transports.get(i);
            peer.checkInterested();
            if (peer.isSnubbed())
                cntPeersSnubbed++;
        }
        setNbPeersSnubbed(cntPeersSnubbed);
    }

    /**
     * Private method to process the results given by DiskManager's piece checking thread via asyncPieceChecked(..)
     */
    private void processPieceChecks() {
        if (piece_check_result_list.size() > 0) {

            final List pieces;

            // process complete piece results

            try {
                piece_check_result_list_mon.enter();

                pieces = new ArrayList(piece_check_result_list);

                piece_check_result_list.clear();

            } finally {

                piece_check_result_list_mon.exit();
            }

            final Iterator it = pieces.iterator();

            while (it.hasNext()) {

                final Object[] data = (Object[]) it.next();

                // bah

                processPieceCheckResult((DiskManagerCheckRequest) data[0], ((Integer) data[1]).intValue());

            }
        }
    }

    private void checkBadPieces() {
        if (mainloop_loop_count % MAINLOOP_SIXTY_SECOND_INTERVAL == 0) {

            if (bad_piece_reported != -1) {

                DiskManagerCheckRequest req = disk_mgr.createCheckRequest(bad_piece_reported, new Integer(CHECK_REASON_BAD_PIECE_CHECK));

                req.setLowPriority(true);

                if (Logger.isEnabled()) {

                    Logger.log(new LogEvent(disk_mgr.getTorrent(), LOGID, "Rescanning reported-bad piece " + bad_piece_reported));

                }

                bad_piece_reported = -1;

                try {
                    disk_mgr.enqueueCheckRequest(req, this);

                } catch (Throwable e) {

                    Debug.printStackTrace(e);
                }
            }
        }
    }

    private void checkRescan() {
        if (rescan_piece_time == 0) {

            // pending a piece completion

            return;
        }

        if (next_rescan_piece == -1) {

            if (mainloop_loop_count % MAINLOOP_FIVE_SECOND_INTERVAL == 0) {

                if (adapter.isPeriodicRescanEnabled()) {

                    next_rescan_piece = 0;
                }
            }
        } else {

            if (mainloop_loop_count % MAINLOOP_TEN_MINUTE_INTERVAL == 0) {

                if (!adapter.isPeriodicRescanEnabled()) {

                    next_rescan_piece = -1;
                }
            }
        }

        if (next_rescan_piece == -1) {

            return;
        }

        // delay as required

        final long now = SystemTime.getCurrentTime();

        if (rescan_piece_time > now) {

            rescan_piece_time = now;
        }

        // 250K/sec limit

        final long piece_size = disk_mgr.getPieceLength();

        final long millis_per_piece = piece_size / 250;

        if (now - rescan_piece_time < millis_per_piece) {

            return;
        }

        while (next_rescan_piece != -1) {

            int this_piece = next_rescan_piece;

            next_rescan_piece++;

            if (next_rescan_piece == _nbPieces) {

                next_rescan_piece = -1;
            }

            // this functionality is to pick up pieces that have been downloaded OUTSIDE of
            // Azureus - e.g. when two torrents are sharing a single file. Hence the check on
            // the piece NOT being done

            if (pePieces[this_piece] == null && !dm_pieces[this_piece].isDone() && dm_pieces[this_piece].isNeeded()) {

                DiskManagerCheckRequest req = disk_mgr.createCheckRequest(this_piece, new Integer(CHECK_REASON_SCAN));

                req.setLowPriority(true);

                if (Logger.isEnabled()) {

                    Logger.log(new LogEvent(disk_mgr.getTorrent(), LOGID, "Rescanning piece " + this_piece));

                }

                rescan_piece_time = 0; // mark as check piece in process

                try {
                    disk_mgr.enqueueCheckRequest(req, this);

                } catch (Throwable e) {

                    rescan_piece_time = now;

                    Debug.printStackTrace(e);
                }

                break;
            }
        }
    }

    public void badPieceReported(PEPeerTransport originator, int piece_number) {
        Debug.outNoStack(getDisplayName() + ": bad piece #" + piece_number + " reported by " + originator.getIp());

        if (piece_number < 0 || piece_number >= _nbPieces) {

            return;
        }

        bad_piece_reported = piece_number;
    }

    private static final int FE_EVENT_LIMIT = 5; // don't make > 15 without changing bloom!S

    /*
     * We keep track of both peer connection events and attempts to re-download the same fast piece for a given peer to prevent an attack whereby a
     * peer connects and repeatedly downloads the same fast piece, or alternatively connects, downloads some fast pieces, disconnects, then does so
     * again.
     */

    public boolean isFastExtensionPermitted(PEPeerTransport originator) {
        try {
            byte[] key = originator.getIp().getBytes(Constants.BYTE_ENCODING);

            synchronized (naughty_fast_extension_bloom) {

                int events = naughty_fast_extension_bloom.add(key);

                if (events < FE_EVENT_LIMIT) {

                    return (true);
                }

                Logger.log(new LogEvent(disk_mgr.getTorrent(), LOGID, "Fast extension disabled for " + originator.getIp()
                        + " due to repeat connections"));
            }

        } catch (Throwable e) {
        }

        return (false);
    }

    public void reportBadFastExtensionUse(PEPeerTransport originator) {
        try {
            byte[] key = originator.getIp().getBytes(Constants.BYTE_ENCODING);

            synchronized (naughty_fast_extension_bloom) {

                if (naughty_fast_extension_bloom.add(key) == FE_EVENT_LIMIT) {

                    Logger.log(new LogEvent(disk_mgr.getTorrent(), LOGID, "Fast extension disabled for " + originator.getIp()
                            + " due to repeat requests for the same pieces"));
                }
            }
        } catch (Throwable e) {
        }
    }

    public void setStatsReceiver(PEPeerManager.StatsReceiver receiver) {
        stats_receiver = receiver;
    }

    public void statsRequest(PEPeerTransport originator, Map request) {
        Map reply = new HashMap();

        adapter.statsRequest(originator, request, reply);

        if (reply.size() > 0) {

            originator.sendStatsReply(reply);
        }
    }

    public void statsReply(PEPeerTransport originator, Map reply) {
        PEPeerManager.StatsReceiver receiver = stats_receiver;

        if (receiver != null) {

            receiver.receiveStats(originator, reply);
        }
    }

    /**
     * This method checks if the downloading process is finished.
     * 
     */

    private void checkFinished(final boolean start_of_day) {
        final boolean all_pieces_done = disk_mgr.getRemainingExcludingDND() == 0;

        if (all_pieces_done) {

            seeding_mode = true;

            prefer_udp_bloom = null;

            piecePicker.clearEndGameChunks();

            if (!start_of_day)
                adapter.setStateFinishing();

            _timeFinished = SystemTime.getCurrentTime();
            final ArrayList peer_transports = peer_transports_cow;

            // remove previous snubbing
            for (int i = 0; i < peer_transports.size(); i++) {
                final PEPeerTransport pc = (PEPeerTransport) peer_transports.get(i);
                pc.setSnubbed(false);
            }
            setNbPeersSnubbed(0);

            final boolean checkPieces = COConfigurationManager.getBooleanParameter("Check Pieces on Completion");

            // re-check all pieces to make sure they are not corrupt, but only if we weren't already complete
            if (checkPieces && !start_of_day) {
                final DiskManagerCheckRequest req = disk_mgr.createCheckRequest(-1, new Integer(CHECK_REASON_COMPLETE));
                disk_mgr.enqueueCompleteRecheckRequest(req, this);
            }

            _timeStartedSeeding = SystemTime.getCurrentTime();
            _timeStartedSeeding_mono = SystemTime.getMonotonousTime();

            try {
                disk_mgr.saveResumeData(false);

            } catch (Throwable e) {
                Debug.out("Failed to save resume data", e);
            }

            adapter.setStateSeeding(start_of_day);

            final AESemaphore waiting_it = new AESemaphore("PEC:DE");

            new AEThread2("PEC:DE") {
                public void run() {
                    try {
                        disk_mgr.downloadEnded(new DiskManager.OperationStatus() {
                            public void gonnaTakeAWhile(GettingThere gt) {
                                boolean async_set = false;

                                synchronized (PEPeerControlImpl.this) {

                                    if (finish_in_progress == null) {

                                        finish_in_progress = gt;

                                        async_set = true;
                                    }
                                }

                                if (async_set) {

                                    waiting_it.release();
                                }
                            }
                        });
                    } finally {

                        waiting_it.release();
                    }
                }
            }.start();

            waiting_it.reserve();

        } else {

            seeding_mode = false;
        }
    }

    protected void checkCompletionState() {
        if (mainloop_loop_count % MAINLOOP_ONE_SECOND_INTERVAL != 0) {

            return;
        }

        boolean dm_done = disk_mgr.getRemainingExcludingDND() == 0;

        if (seeding_mode) {

            if (!dm_done) {

                seeding_mode = false;

                _timeStartedSeeding = -1;
                _timeStartedSeeding_mono = -1;
                _timeFinished = 0;

                Logger.log(new LogEvent(disk_mgr.getTorrent(), LOGID, "Turning off seeding mode for PEPeerManager"));
            }

        } else {

            if (dm_done) {

                checkFinished(false);

                if (seeding_mode) {

                    Logger.log(new LogEvent(disk_mgr.getTorrent(), LOGID, "Turning on seeding mode for PEPeerManager"));

                }
            }
        }
    }

    /**
     * This method will locate expired requests on peers, will cancel them, and mark the peer as snubbed if we haven't received usefull data from them
     * within the last 60 seconds
     */
    private void checkRequests() {
        // to be honest I don't see why this can't be 5 seconds, but I'm trying 1 second
        // now as the existing 0.1 second is crazy given we're checking for events that occur
        // at 60+ second intervals

        if (mainloop_loop_count % MAINLOOP_ONE_SECOND_INTERVAL != 0) {

            return;
        }

        final long now = SystemTime.getCurrentTime();

        // for every connection
        final ArrayList peer_transports = peer_transports_cow;
        for (int i = peer_transports.size() - 1; i >= 0; i--) {
            final PEPeerTransport pc = (PEPeerTransport) peer_transports.get(i);
            if (pc.getPeerState() == PEPeer.TRANSFERING) {
                final List expired = pc.getExpiredRequests();
                if (expired != null && expired.size() > 0) { // now we know there's a request that's > 60 seconds old
                    final boolean isSeed = pc.isSeed();
                    // snub peers that haven't sent any good data for a minute
                    final long timeSinceGoodData = pc.getTimeSinceGoodDataReceived();
                    if (timeSinceGoodData < 0 || timeSinceGoodData > 60 * 1000)
                        pc.setSnubbed(true);

                    // Only cancel first request if more than 2 mins have passed
                    DiskManagerReadRequest request = (DiskManagerReadRequest) expired.get(0);

                    final long timeSinceData = pc.getTimeSinceLastDataMessageReceived();
                    final boolean noData = (timeSinceData < 0) || timeSinceData > (1000 * (isSeed ? 120 : 60));
                    final long timeSinceOldestRequest = now - request.getTimeCreated(now);

                    // for every expired request
                    for (int j = (timeSinceOldestRequest > 120 * 1000 && noData) ? 0 : 1; j < expired.size(); j++) {
                        // get the request object
                        request = (DiskManagerReadRequest) expired.get(j);
                        // Only cancel first request if more than 2 mins have passed
                        pc.sendCancel(request); // cancel the request object
                        // get the piece number
                        final int pieceNumber = request.getPieceNumber();
                        PEPiece pe_piece = pePieces[pieceNumber];
                        // unmark the request on the block
                        if (pe_piece != null)
                            pe_piece.clearRequested(request.getOffset() / DiskManager.BLOCK_SIZE);
                        // remove piece if empty so peers can choose something else, except in end game
                        if (!piecePicker.isInEndGameMode())
                            checkEmptyPiece(pieceNumber);
                    }
                }
            }
        }
    }

    private void updateTrackerAnnounceInterval() {
        if (mainloop_loop_count % MAINLOOP_FIVE_SECOND_INTERVAL != 0) {
            return;
        }

        final int WANT_LIMIT = 100;

        int num_wanted = getMaxNewConnectionsAllowed();

        final boolean has_remote = adapter.isNATHealthy();
        if (has_remote) {
            // is not firewalled, so can accept incoming connections,
            // which means no need to continually keep asking the tracker for peers
            num_wanted = (int) (num_wanted / 1.5);
        }

        if (num_wanted < 0 || num_wanted > WANT_LIMIT) {
            num_wanted = WANT_LIMIT;
        }

        int current_connection_count = PeerIdentityManager.getIdentityCount(_hash);

        final TRTrackerScraperResponse tsr = adapter.getTrackerScrapeResponse();

        if (tsr != null && tsr.isValid()) { // we've got valid scrape info
            final int num_seeds = tsr.getSeeds();
            final int num_peers = tsr.getPeers();

            final int swarm_size;

            if (seeding_mode) {
                // Only use peer count when seeding, as other seeds are unconnectable.
                // Since trackers return peers randomly (some of which will be seeds),
                // backoff by the seed2peer ratio since we're given only that many peers
                // on average each announce.
                final float ratio = (float) num_peers / (num_seeds + num_peers);
                swarm_size = (int) (num_peers * ratio);
            } else {
                swarm_size = num_peers + num_seeds;
            }

            if (swarm_size < num_wanted) { // lower limit to swarm size if necessary
                num_wanted = swarm_size;
            }
        }

        if (num_wanted < 1) { // we dont need any more connections
            adapter.setTrackerRefreshDelayOverrides(100); // use normal announce interval
            return;
        }

        if (current_connection_count == 0)
            current_connection_count = 1; // fudge it :)

        final int current_percent = (current_connection_count * 100) / (current_connection_count + num_wanted);

        adapter.setTrackerRefreshDelayOverrides(current_percent); // set dynamic interval override
    }

    public boolean hasDownloadablePiece() {
        return (piecePicker.hasDownloadablePiece());
    }

    public int getBytesQueuedForUpload() {
        return (bytes_queued_for_upload);
    }

    public int getNbPeersWithUploadQueued() {
        return (connections_with_queued_data);
    }

    public int getNbPeersWithUploadBlocked() {
        return (connections_with_queued_data_blocked);
    }

    public int getNbPeersUnchoked() {
        return (connections_unchoked);
    }

    public int[] getAvailability() {
        return piecePicker.getAvailability();
    }

    // this only gets called when the My Torrents view is displayed
    public float getMinAvailability() {
        return piecePicker.getMinAvailability();
    }

    public float getMinAvailability(int file_index) {
        return piecePicker.getMinAvailability(file_index);
    }

    public long getBytesUnavailable() {
        return piecePicker.getBytesUnavailable();
    }

    public float getAvgAvail() {
        return piecePicker.getAvgAvail();
    }

    public long getAvailWentBadTime() {
        long went_bad = piecePicker.getAvailWentBadTime();

        // there's a chance a seed connects and then disconnects (when we're seeding) quickly
        // enough for the piece picker not to notice...

        if (piecePicker.getMinAvailability() < 1.0 && last_seed_disconnect_time > went_bad - 5000) {

            went_bad = last_seed_disconnect_time;
        }

        return (went_bad);
    }

    public void addPeerTransport(PEPeerTransport transport) {
        if (!ip_filter.isInRange(transport.getIp(), getDisplayName(), getTorrentHash())) {
            final ArrayList peer_transports = peer_transports_cow;

            if (!peer_transports.contains(transport)) {
                addToPeerTransports(transport);
            } else {
                Debug.out("addPeerTransport():: peer_transports.contains(transport): SHOULD NEVER HAPPEN !");
                transport.closeConnection("already connected");
            }
        } else {
            transport.closeConnection("IP address blocked by filters");
        }
    }

    /**
     * Do all peer choke/unchoke processing.
     */
    private void doUnchokes() {

        // logic below is either 1 second or 10 secondly, bail out early id neither

        if (!UploadSlotManager.AUTO_SLOT_ENABLE) { // manual per-torrent unchoke slot mode

            if (mainloop_loop_count % MAINLOOP_ONE_SECOND_INTERVAL != 0) {
                return;
            }

            final int max_to_unchoke = adapter.getMaxUploads(); // how many simultaneous uploads we should consider
            final ArrayList peer_transports = peer_transports_cow;

            // determine proper unchoker
            if (seeding_mode) {
                if (unchoker == null || !(unchoker.isSeedingUnchoker())) {
                    unchoker = UnchokerFactory.getSingleton().getUnchoker(true);
                }
            } else {
                if (unchoker == null || unchoker.isSeedingUnchoker()) {
                    unchoker = UnchokerFactory.getSingleton().getUnchoker(false);
                }
            }

            // do main choke/unchoke update every 10 secs

            if (mainloop_loop_count % MAINLOOP_TEN_SECOND_INTERVAL == 0) {

                final boolean refresh = mainloop_loop_count % MAINLOOP_THIRTY_SECOND_INTERVAL == 0;

                boolean do_high_latency_peers = mainloop_loop_count % MAINLOOP_TWENTY_SECOND_INTERVAL == 0;

                if (do_high_latency_peers) {

                    boolean ok = false;

                    for (String net : AENetworkClassifier.AT_NON_PUBLIC) {

                        if (adapter.isNetworkEnabled(net)) {

                            ok = true;

                            break;
                        }
                    }

                    if (!ok) {

                        do_high_latency_peers = false;
                    }
                }

                unchoker.calculateUnchokes(max_to_unchoke, peer_transports, refresh, adapter.hasPriorityConnection(), do_high_latency_peers);

                ArrayList chokes = unchoker.getChokes();
                ArrayList unchokes = unchoker.getUnchokes();

                addFastUnchokes(unchokes);

                UnchokerUtil.performChokes(chokes, unchokes);

            } else if (mainloop_loop_count % MAINLOOP_ONE_SECOND_INTERVAL == 0) { // do quick unchoke check every 1 sec

                ArrayList unchokes = unchoker.getImmediateUnchokes(max_to_unchoke, peer_transports);

                addFastUnchokes(unchokes);

                UnchokerUtil.performChokes(null, unchokes);
            }
        }
    }

    private void addFastUnchokes(ArrayList peers_to_unchoke) {
        for (Iterator it = peer_transports_cow.iterator(); it.hasNext();) {

            PEPeerTransport peer = (PEPeerTransport) it.next();

            if (peer.getConnectionState() != PEPeerTransport.CONNECTION_FULLY_ESTABLISHED || !UnchokerUtil.isUnchokable(peer, true)
                    || peers_to_unchoke.contains(peer)) {

                continue;
            }

            if (peer.isLANLocal()) {

                peers_to_unchoke.add(peer);

            } else if (fast_unchoke_new_peers && peer.getData("fast_unchoke_done") == null) {

                peer.setData("fast_unchoke_done", "");

                peers_to_unchoke.add(peer);
            }
        }
    }

    // send the have requests out
    private void sendHave(int pieceNumber) {
        // fo
        final ArrayList peer_transports = peer_transports_cow;

        for (int i = 0; i < peer_transports.size(); i++) {
            // get a peer connection
            final PEPeerTransport pc = (PEPeerTransport) peer_transports.get(i);
            // send the have message
            pc.sendHave(pieceNumber);
        }

    }

    // Method that checks if we are connected to another seed, and if so, disconnect from him.
    private void checkSeeds() {
        // proceed on mainloop 1 second intervals if we're a seed and we want to force disconnects
        if ((mainloop_loop_count % MAINLOOP_ONE_SECOND_INTERVAL) != 0)
            return;

        if (!disconnect_seeds_when_seeding) {
            return;
        }

        ArrayList to_close = null;

        final ArrayList peer_transports = peer_transports_cow;
        for (int i = 0; i < peer_transports.size(); i++) {
            final PEPeerTransport pc = (PEPeerTransport) peer_transports.get(i);

            if (pc != null && pc.getPeerState() == PEPeer.TRANSFERING && ((isSeeding() && pc.isSeed()) || pc.isRelativeSeed())) {
                if (to_close == null)
                    to_close = new ArrayList();
                to_close.add(pc);
            }
        }

        if (to_close != null) {
            for (int i = 0; i < to_close.size(); i++) {
                closeAndRemovePeer((PEPeerTransport) to_close.get(i), "disconnect other seed when seeding", false);
            }
        }
    }

    private void updateStats() {

        if ((mainloop_loop_count % MAINLOOP_ONE_SECOND_INTERVAL) != 0) {
            return;
        }

        stats_tick_count++;

        // calculate seeds vs peers
        final ArrayList<PEPeer> peer_transports = peer_transports_cow;

        int new_pending_tcp_connections = 0;
        int new_connecting_tcp_connections = 0;

        int new_seeds = 0;
        int new_peers = 0;
        int new_tcp_incoming = 0;
        int new_udp_incoming = 0;
        int new_utp_incoming = 0;

        int bytes_queued = 0;
        int con_queued = 0;
        int con_blocked = 0;
        int con_unchoked = 0;

        for (Iterator<PEPeer> it = peer_transports.iterator(); it.hasNext();) {

            final PEPeerTransport pc = (PEPeerTransport) it.next();

            if (pc.getPeerState() == PEPeer.TRANSFERING) {

                if (!pc.isChokedByMe()) {

                    con_unchoked++;
                }

                Connection connection = pc.getPluginConnection();

                if (connection != null) {

                    OutgoingMessageQueue mq = connection.getOutgoingMessageQueue();

                    int q = mq.getDataQueuedBytes() + mq.getProtocolQueuedBytes();

                    bytes_queued += q;

                    if (q > 0) {

                        con_queued++;

                        if (mq.isBlocked()) {

                            con_blocked++;
                        }
                    }
                }

                if (pc.isSeed())
                    new_seeds++;
                else
                    new_peers++;

                if (pc.isIncoming() && !pc.isLANLocal()) {

                    if (pc.isTCP()) {

                        new_tcp_incoming++;

                    } else {

                        String protocol = pc.getProtocol();

                        if (protocol.equals("UDP")) {

                            new_udp_incoming++;

                        } else {

                            new_utp_incoming++;
                        }
                    }
                }
            } else {
                if (pc.isTCP()) {

                    int c_state = pc.getConnectionState();

                    if (c_state == PEPeerTransport.CONNECTION_PENDING) {

                        new_pending_tcp_connections++;

                    } else if (c_state == PEPeerTransport.CONNECTION_CONNECTING) {

                        new_connecting_tcp_connections++;
                    }
                }
            }
        }

        _seeds = new_seeds;
        _peers = new_peers;
        _remotesTCPNoLan = new_tcp_incoming;
        _remotesUDPNoLan = new_udp_incoming;
        _remotesUTPNoLan = new_utp_incoming;
        _tcpPendingConnections = new_pending_tcp_connections;
        _tcpConnectingConnections = new_connecting_tcp_connections;

        bytes_queued_for_upload = bytes_queued;
        connections_with_queued_data = con_queued;
        connections_with_queued_data_blocked = con_blocked;
        connections_unchoked = con_unchoked;

        _stats.update(stats_tick_count);
    }

    /**
     * The way to unmark a request as being downloaded, or also called by Peer connections objects when connection is closed or choked
     * 
     * @param request
     *            a DiskManagerReadRequest holding details of what was canceled
     */
    public void requestCanceled(DiskManagerReadRequest request) {
        final int pieceNumber = request.getPieceNumber(); // get the piece number
        PEPiece pe_piece = pePieces[pieceNumber];
        if (pe_piece != null) {
            pe_piece.clearRequested(request.getOffset() / DiskManager.BLOCK_SIZE);
        }
    }

    public PEPeerControl getControl() {
        return (this);
    }

    public byte[][] getSecrets(int crypto_level) {
        return (adapter.getSecrets(crypto_level));
    }

    // get the hash value
    public byte[] getHash() {
        return _hash.getDataID();
    }

    public PeerIdentityDataID getPeerIdentityDataID() {
        return (_hash);
    }

    // get the peer id value
    public byte[] getPeerId() {
        return _myPeerId;
    }

    // get the remaining percentage
    public long getRemaining() {
        return disk_mgr.getRemaining();
    }

    public void discarded(PEPeer peer, int length) {
        if (length > 0) {
            _stats.discarded(peer, length);

            // discards are more likely during end-game-mode

            if (ban_peer_discard_ratio > 0 && !(piecePicker.isInEndGameMode() || piecePicker.hasEndGameModeBeenAbandoned())) {

                long received = peer.getStats().getTotalDataBytesReceived();
                long discarded = peer.getStats().getTotalBytesDiscarded();

                long non_discarded = received - discarded;

                if (non_discarded < 0) {

                    non_discarded = 0;
                }

                if (discarded >= ban_peer_discard_min_kb * 1024L) {

                    if (non_discarded == 0 || ((float) discarded) / non_discarded >= ban_peer_discard_ratio) {

                        badPeerDetected(peer.getIp(), -1);
                    }
                }
            }
        }
    }

    public void dataBytesReceived(PEPeer peer, int length) {
        if (length > 0) {
            _stats.dataBytesReceived(peer, length);

            _averageReceptionSpeed.addValue(length);
        }
    }

    public void protocolBytesReceived(PEPeer peer, int length) {
        if (length > 0) {
            _stats.protocolBytesReceived(peer, length);
        }
    }

    public void dataBytesSent(PEPeer peer, int length) {
        if (length > 0) {
            _stats.dataBytesSent(peer, length);
        }
    }

    public void protocolBytesSent(PEPeer peer, int length) {
        if (length > 0) {
            _stats.protocolBytesSent(peer, length);
        }
    }

    /**
     * DiskManagerWriteRequestListener message
     * 
     * @see org.gudy.azureus2.core3.disk.DiskManagerWriteRequestListener
     */
    public void writeCompleted(DiskManagerWriteRequest request) {
        final int pieceNumber = request.getPieceNumber();

        DiskManagerPiece dm_piece = dm_pieces[pieceNumber];

        if (!dm_piece.isDone()) {

            final PEPiece pePiece = pePieces[pieceNumber];

            if (pePiece != null) {

                pePiece.setWritten((PEPeer) request.getUserData(), request.getOffset() / DiskManager.BLOCK_SIZE);

            } else {

                // this is a way of fixing a 99.9% bug where a dmpiece is left in a
                // fully downloaded state with the underlying pe_piece null. Possible explanation is
                // that a slow peer sends an entire piece at around the time a pe_piece gets reset
                // due to inactivity.

                // we also get here when recovering data that has come in late after the piece has
                // been abandoned

                dm_piece.setWritten(request.getOffset() / DiskManager.BLOCK_SIZE);
            }
        }
    }

    public void writeFailed(DiskManagerWriteRequest request, Throwable cause) {
        // if the write has failed then the download will have been stopped so there is no need to try
        // and reset the piece
    }

    /**
     * This method will queue up a dism manager write request for the block if the block is not already written. It will send out cancels for the
     * block to all peer either if in end-game mode, or per cancel param
     * 
     * @param pieceNumber
     *            to potentialy write to
     * @param offset
     *            within piece to queue write for
     * @param data
     *            to be writen
     * @param sender
     *            peer that sent this data
     * @param cancel
     *            if cancels definatly need to be sent to all peers for this request
     */
    public void writeBlock(int pieceNumber, int offset, DirectByteBuffer data, PEPeer sender, boolean cancel) {
        final int blockNumber = offset / DiskManager.BLOCK_SIZE;
        final DiskManagerPiece dmPiece = dm_pieces[pieceNumber];
        if (dmPiece.isWritten(blockNumber)) {
            data.returnToPool();
            return;
        }

        PEPiece pe_piece = pePieces[pieceNumber];

        if (pe_piece != null) {

            pe_piece.setDownloaded(offset);
        }

        final DiskManagerWriteRequest request = disk_mgr.createWriteRequest(pieceNumber, offset, data, sender);
        disk_mgr.enqueueWriteRequest(request, this);
        // In case we are in endGame mode, remove the block from the chunk list
        if (piecePicker.isInEndGameMode())
            piecePicker.removeFromEndGameModeChunks(pieceNumber, offset);
        if (cancel || piecePicker.isInEndGameMode()) { // cancel any matching outstanding download requests
                                                       // For all connections cancel the request
            final ArrayList peer_transports = peer_transports_cow;
            for (int i = 0; i < peer_transports.size(); i++) {
                final PEPeerTransport connection = (PEPeerTransport) peer_transports.get(i);
                final DiskManagerReadRequest dmr = disk_mgr.createReadRequest(pieceNumber, offset, dmPiece.getBlockSize(blockNumber));
                connection.sendCancel(dmr);
            }
        }
    }

    // /**
    // * This method is only called when a block is received after the initial request expired,
    // * but the data has not yet been fulfilled by any other peer, so we use the block data anyway
    // * instead of throwing it away, and cancel any outstanding requests for that block that might have
    // * been sent after initial expiry.
    // */
    // public void writeBlockAndCancelOutstanding(int pieceNumber, int offset, DirectByteBuffer data,PEPeer sender) {
    // final int blockNumber =offset /DiskManager.BLOCK_SIZE;
    // final DiskManagerPiece dmPiece =dm_pieces[pieceNumber];
    // if (dmPiece.isWritten(blockNumber))
    // {
    // data.returnToPool();
    // return;
    // }
    // DiskManagerWriteRequest request =disk_mgr.createWriteRequest(pieceNumber, offset, data, sender);
    // disk_mgr.enqueueWriteRequest(request, this);

    // // cancel any matching outstanding download requests
    // List peer_transports =peer_transports_cow;
    // for (int i =0; i <peer_transports.size(); i++)
    // {
    // PEPeerTransport connection =(PEPeerTransport) peer_transports.get(i);
    // DiskManagerReadRequest dmr =disk_mgr.createReadRequest(pieceNumber, offset, dmPiece.getBlockSize(blockNumber));
    // connection.sendCancel(dmr);
    // }
    // }

    public boolean isWritten(int piece_number, int offset) {
        return dm_pieces[piece_number].isWritten(offset / DiskManager.BLOCK_SIZE);
    }

    public boolean validateReadRequest(PEPeerTransport originator, int pieceNumber, int offset, int length) {
        if (disk_mgr.checkBlockConsistencyForRead(originator.getClient() + ": " + originator.getIp(), true, pieceNumber, offset, length)) {

            if (enable_seeding_piece_rechecks && isSeeding()) {

                DiskManagerPiece dm_piece = dm_pieces[pieceNumber];

                int read_count = dm_piece.getReadCount() & 0xffff;

                if (read_count < SEED_CHECK_WAIT_MARKER - 1) {

                    read_count++;

                    dm_piece.setReadCount((short) read_count);
                }
            }

            return (true);
        } else {

            return (false);
        }
    }

    public boolean validateHintRequest(PEPeerTransport originator, int pieceNumber, int offset, int length) {
        return (disk_mgr.checkBlockConsistencyForHint(originator.getClient() + ": " + originator.getIp(), pieceNumber, offset, length));
    }

    public boolean validatePieceReply(PEPeerTransport originator, int pieceNumber, int offset, DirectByteBuffer data) {
        return disk_mgr.checkBlockConsistencyForWrite(originator.getClient() + ": " + originator.getIp(), pieceNumber, offset, data);
    }

    public int getAvailability(int pieceNumber) {
        return piecePicker.getAvailability(pieceNumber);
    }

    public void havePiece(int pieceNumber, int pieceLength, PEPeer pcOrigin) {
        piecePicker.addHavePiece(pcOrigin, pieceNumber);
        _stats.haveNewPiece(pieceLength);

        if (superSeedMode) {
            superSeedPieces[pieceNumber].peerHasPiece(pcOrigin);
            if (pieceNumber == pcOrigin.getUniqueAnnounce()) {
                pcOrigin.setUniqueAnnounce(-1);
                superSeedModeNumberOfAnnounces--;
            }
        }
        int availability = piecePicker.getAvailability(pieceNumber) - 1;
        if (availability < 4) {
            if (dm_pieces[pieceNumber].isDone())
                availability--;
            if (availability <= 0)
                return;
            // for all peers

            final ArrayList peer_transports = peer_transports_cow;

            for (int i = peer_transports.size() - 1; i >= 0; i--) {
                final PEPeerTransport pc = (PEPeerTransport) peer_transports.get(i);
                if (pc != pcOrigin && pc.getPeerState() == PEPeer.TRANSFERING && pc.isPieceAvailable(pieceNumber))
                    ((PEPeerStatsImpl) pc.getStats()).statisticalSentPiece(pieceLength / availability);
            }
        }
    }

    public int getPieceLength(int pieceNumber) {

        return disk_mgr.getPieceLength(pieceNumber);

    }

    public int getNbPeers() {
        return _peers;
    }

    public int getNbSeeds() {
        return _seeds;
    }

    public int getNbRemoteTCPConnections() {
        return _remotesTCPNoLan;
    }

    public int getNbRemoteUDPConnections() {
        return _remotesUDPNoLan;
    }

    public int getNbRemoteUTPConnections() {
        return _remotesUTPNoLan;
    }

    public long getLastRemoteConnectionTime() {
        return (last_remote_time);
    }

    public PEPeerManagerStats getStats() {
        return _stats;
    }

    public int getNbPeersStalledPendingLoad() {
        int res = 0;

        Iterator it = peer_transports_cow.iterator();

        while (it.hasNext()) {

            PEPeerTransport transport = (PEPeerTransport) it.next();

            if (transport.isStalledPendingLoad()) {

                res++;
            }
        }

        return (res);
    }

    /**
     * Returns the ETA time in seconds. 0 = download is complete. < 0 = download is complete and it took -xxx time to complete.
     * Constants.CRAPPY_INFINITE_AS_LONG = incomplete and 0 average speed
     */
    public long getETA(boolean smoothed) {
        final long now = SystemTime.getCurrentTime();

        if (now < last_eta_calculation || now - last_eta_calculation > 900) {

            long dataRemaining = disk_mgr.getRemainingExcludingDND();

            if (dataRemaining > 0) {

                int writtenNotChecked = 0;

                for (int i = 0; i < _nbPieces; i++) {
                    if (dm_pieces[i].isInteresting()) {
                        writtenNotChecked += dm_pieces[i].getNbWritten() * DiskManager.BLOCK_SIZE;
                    }
                }

                dataRemaining = dataRemaining - writtenNotChecked;

                if (dataRemaining < 0) {
                    dataRemaining = 0;
                }
            }

            long jagged_result;
            long smooth_result;

            if (dataRemaining == 0) {
                final long timeElapsed = (_timeFinished - _timeStarted) / 1000;
                // if time was spent downloading....return the time as negative
                if (timeElapsed > 1) {
                    jagged_result = timeElapsed * -1;
                } else {
                    jagged_result = 0;
                }
                smooth_result = jagged_result;
            } else {

                {
                    final long averageSpeed = _averageReceptionSpeed.getAverage();
                    long lETA = (averageSpeed == 0) ? Constants.CRAPPY_INFINITE_AS_LONG : dataRemaining / averageSpeed;
                    // stop the flickering of ETA from "Finished" to "x seconds" when we are
                    // just about complete, but the data rate is jumpy.
                    if (lETA == 0)
                        lETA = 1;
                    jagged_result = lETA;
                }
                {
                    final long averageSpeed = _stats.getSmoothedDataReceiveRate();
                    long lETA = (averageSpeed == 0) ? Constants.CRAPPY_INFINITE_AS_LONG : dataRemaining / averageSpeed;
                    // stop the flickering of ETA from "Finished" to "x seconds" when we are
                    // just about complete, but the data rate is jumpy.
                    if (lETA == 0)
                        lETA = 1;
                    smooth_result = lETA;
                }
            }

            last_eta = jagged_result;
            last_eta_smoothed = smooth_result;
            last_eta_calculation = now;
        }

        return (smoothed ? last_eta_smoothed : last_eta);
    }

    public boolean isRTA() {
        return (piecePicker.getRTAProviders().size() > 0);
    }

    private void addToPeerTransports(PEPeerTransport peer) {
        boolean added = false;

        List limiters;

        try {
            peer_transports_mon.enter();

            // if it is already disconnected (synchronous failure during connect
            // for example) don't add it

            if (peer.getPeerState() == PEPeer.DISCONNECTED) {

                return;
            }

            if (peer_transports_cow.contains(peer)) {
                Debug.out("Transport added twice");
                return; // we do not want to close it
            }

            if (is_running) {
                // copy-on-write semantics
                final ArrayList new_peer_transports = new ArrayList(peer_transports_cow.size() + 1);

                new_peer_transports.addAll(peer_transports_cow);

                new_peer_transports.add(peer);

                peer_transports_cow = new_peer_transports;

                added = true;
            }

            limiters = external_rate_limiters_cow;
        } finally {
            peer_transports_mon.exit();
        }

        if (added) {
            boolean incoming = peer.isIncoming();

            _stats.haveNewConnection(incoming);

            if (incoming) {
                long connect_time = SystemTime.getCurrentTime();

                if (connect_time > last_remote_time) {

                    last_remote_time = connect_time;
                }
            }

            if (limiters != null) {

                for (int i = 0; i < limiters.size(); i++) {

                    Object[] entry = (Object[]) limiters.get(i);

                    peer.addRateLimiter((LimitedRateGroup) entry[0], ((Boolean) entry[1]).booleanValue());
                }
            }

            peerAdded(peer);
        } else {
            peer.closeConnection("PeerTransport added when manager not running");
        }
    }

    public void addRateLimiter(LimitedRateGroup group, boolean upload) {
        List<PEPeer> transports;

        try {
            peer_transports_mon.enter();

            ArrayList<Object[]> new_limiters =
                    new ArrayList<Object[]>(external_rate_limiters_cow == null ? 1 : external_rate_limiters_cow.size() + 1);

            if (external_rate_limiters_cow != null) {

                new_limiters.addAll(external_rate_limiters_cow);
            }

            new_limiters.add(new Object[] { group, new Boolean(upload) });

            external_rate_limiters_cow = new_limiters;

            transports = peer_transports_cow;

        } finally {

            peer_transports_mon.exit();
        }

        for (int i = 0; i < transports.size(); i++) {

            transports.get(i).addRateLimiter(group, upload);
        }
    }

    public void removeRateLimiter(LimitedRateGroup group, boolean upload) {
        List transports;

        try {
            peer_transports_mon.enter();

            if (external_rate_limiters_cow != null) {

                ArrayList new_limiters = new ArrayList(external_rate_limiters_cow.size() - 1);

                for (int i = 0; i < external_rate_limiters_cow.size(); i++) {

                    Object[] entry = (Object[]) external_rate_limiters_cow.get(i);

                    if (entry[0] != group) {

                        new_limiters.add(entry);
                    }
                }

                if (new_limiters.size() == 0) {

                    external_rate_limiters_cow = null;

                } else {

                    external_rate_limiters_cow = new_limiters;
                }
            }

            transports = peer_transports_cow;

        } finally {

            peer_transports_mon.exit();
        }

        for (int i = 0; i < transports.size(); i++) {

            ((PEPeerTransport) transports.get(i)).removeRateLimiter(group, upload);
        }
    }

    public int getUploadRateLimitBytesPerSecond() {
        return (adapter.getUploadRateLimitBytesPerSecond());
    }

    public int getDownloadRateLimitBytesPerSecond() {
        return (adapter.getDownloadRateLimitBytesPerSecond());
    }

    // the peer calls this method itself in closeConnection() to notify this manager

    public void peerConnectionClosed(PEPeerTransport peer, boolean connect_failed, boolean network_failed) {
        boolean connection_found = false;

        boolean tcpReconnect = false;
        boolean ipv6reconnect = false;

        try {
            peer_transports_mon.enter();

            int udpPort = peer.getUDPListenPort();

            boolean canTryUDP = UDPNetworkManager.UDP_OUTGOING_ENABLED && peer.getUDPListenPort() > 0;
            boolean canTryIpv6 = NetworkAdmin.getSingleton().hasIPV6Potential(true) && peer.getAlternativeIPv6() != null;

            if (is_running) {

                PeerItem peer_item = peer.getPeerItemIdentity();
                PeerItem self_item = peer_database.getSelfPeer();

                if (self_item == null || !self_item.equals(peer_item)) {

                    String ip = peer.getIp();
                    boolean wasIPv6;
                    try {

                        wasIPv6 = AddressUtils.getByName(ip) instanceof Inet6Address;
                    } catch (UnknownHostException e) {
                        wasIPv6 = false;
                        // something is fishy about the old address, don't try to reconnect with v6
                        canTryIpv6 = false;
                    }

                    // System.out.println("netfail="+network_failed+", connfail="+connect_failed+", can6="+canTryIpv6+", was6="+wasIPv6);

                    String key = ip + ":" + udpPort;

                    if (peer.isTCP()) {

                        if (connect_failed) {

                            // TCP connect failure, try UDP later if necessary

                            if (canTryUDP && udp_fallback_for_failed_connection) {

                                pending_nat_traversals.put(key, peer);
                            } else if (canTryIpv6 && !wasIPv6) {
                                tcpReconnect = true;
                                ipv6reconnect = true;
                            }
                        } else if (canTryUDP && udp_fallback_for_dropped_connection && network_failed && seeding_mode && peer.isInterested()
                                && !peer.isSeed() && !peer.isRelativeSeed() && peer.getStats().getEstimatedSecondsToCompletion() > 60
                                && FeatureAvailability.isUDPPeerReconnectEnabled()) {

                            if (Logger.isEnabled()) {
                                Logger.log(new LogEvent(peer, LOGID, LogEvent.LT_WARNING, "Unexpected stream closure detected, attempting recovery"));
                            }

                            // System.out.println( "Premature close of stream: " + getDisplayName() + "/" + peer.getIp());

                            udp_reconnects.put(key, peer);

                        } else if (network_failed
                                && peer.isSafeForReconnect()
                                && !(seeding_mode && (peer.isSeed() || peer.isRelativeSeed() || peer.getStats().getEstimatedSecondsToCompletion() < 60))
                                && getMaxConnections() > 0 && getMaxNewConnectionsAllowed() > getMaxConnections() / 3
                                && FeatureAvailability.isGeneralPeerReconnectEnabled()) {

                            tcpReconnect = true;
                        }
                    } else if (connect_failed) {

                        // UDP connect failure

                        if (udp_fallback_for_failed_connection) {

                            if (peer.getData(PEER_NAT_TRAVERSE_DONE_KEY) == null) {

                                // System.out.println( "Direct reconnect failed, attempting NAT traversal" );

                                pending_nat_traversals.put(key, peer);
                            }
                        }
                    }
                }
            }

            if (peer_transports_cow.contains(peer)) {
                final ArrayList new_peer_transports = new ArrayList(peer_transports_cow);
                new_peer_transports.remove(peer);
                peer_transports_cow = new_peer_transports;
                connection_found = true;
            }
        } finally {
            peer_transports_mon.exit();
        }

        if (connection_found) {
            if (peer.getPeerState() != PEPeer.DISCONNECTED) {
                System.out.println("peer.getPeerState() != PEPeer.DISCONNECTED: " + peer.getPeerState());
            }

            peerRemoved(peer); // notify listeners
        }

        if (tcpReconnect)
            peer.reconnect(false, ipv6reconnect);
    }

    public void peerAdded(PEPeer pc) {
        adapter.addPeer(pc); // async downloadmanager notification

        // sync peermanager notification
        final ArrayList peer_manager_listeners = peer_manager_listeners_cow;

        for (int i = 0; i < peer_manager_listeners.size(); i++) {
            ((PEPeerManagerListener) peer_manager_listeners.get(i)).peerAdded(this, pc);
        }
    }

    public void peerRemoved(PEPeer pc) {
        if (is_running && !seeding_mode && (prefer_udp || prefer_udp_default)) {

            int udp = pc.getUDPListenPort();

            if (udp != 0 && udp == pc.getTCPListenPort()) {

                BloomFilter filter = prefer_udp_bloom;

                if (filter == null) {

                    filter = prefer_udp_bloom = BloomFilterFactory.createAddOnly(PREFER_UDP_BLOOM_SIZE);
                }

                if (filter.getEntryCount() < PREFER_UDP_BLOOM_SIZE / 10) {

                    filter.add(pc.getIp().getBytes());
                }
            }
        }

        final int piece = pc.getUniqueAnnounce();
        if (piece != -1 && superSeedMode) {
            superSeedModeNumberOfAnnounces--;
            superSeedPieces[piece].peerLeft();
        }

        int[] reserved_pieces = pc.getReservedPieceNumbers();

        if (reserved_pieces != null) {

            for (int reserved_piece : reserved_pieces) {

                PEPiece pe_piece = pePieces[reserved_piece];

                if (pe_piece != null) {

                    String reserved_by = pe_piece.getReservedBy();

                    if (reserved_by != null && reserved_by.equals(pc.getIp())) {

                        pe_piece.setReservedBy(null);
                    }
                }
            }
        }

        if (pc.isSeed()) {

            last_seed_disconnect_time = SystemTime.getCurrentTime();
        }

        adapter.removePeer(pc); // async downloadmanager notification

        // sync peermanager notification
        final ArrayList peer_manager_listeners = peer_manager_listeners_cow;

        for (int i = 0; i < peer_manager_listeners.size(); i++) {
            ((PEPeerManagerListener) peer_manager_listeners.get(i)).peerRemoved(this, pc);
        }
    }

    /**
     * Don't pass a null to this method. All activations of pieces must go through here.
     * 
     * @param piece
     *            PEPiece invoked; notifications of it's invocation need to be done
     * @param pieceNumber
     *            of the PEPiece
     */
    public void addPiece(final PEPiece piece, final int pieceNumber, PEPeer for_peer) {
        addPiece(piece, pieceNumber, false, for_peer);
    }

    protected void addPiece(final PEPiece piece, final int pieceNumber, final boolean force_add, PEPeer for_peer) {
        if (piece == null || pePieces[pieceNumber] != null) {
            Debug.out("piece state inconsistent");
        }
        pePieces[pieceNumber] = (PEPieceImpl) piece;
        nbPiecesActive++;
        if (is_running || force_add) {
            // deal with possible piece addition by scheduler loop after closdown started
            adapter.addPiece(piece);
        }

        final ArrayList peer_manager_listeners = peer_manager_listeners_cow;

        for (int i = 0; i < peer_manager_listeners.size(); i++) {
            try {
                ((PEPeerManagerListener) peer_manager_listeners.get(i)).pieceAdded(this, piece, for_peer);

            } catch (Throwable e) {

                Debug.printStackTrace(e);
            }
        }
    }

    /**
     * Sends messages to listeners that the piece is no longer active. All closing out (deactivation) of pieces must go through here. The piece will
     * be null upon return.
     * 
     * @param pePiece
     *            PEPiece to remove
     * @param pieceNumber
     *            int
     */
    public void removePiece(PEPiece pePiece, int pieceNumber) {
        if (pePiece != null) {
            adapter.removePiece(pePiece);
        } else {
            pePiece = pePieces[pieceNumber];
        }

        // only decrement num-active if this piece was active (see comment below as to why this might no be the case)

        if (pePieces[pieceNumber] != null) {
            pePieces[pieceNumber] = null;
            nbPiecesActive--;
        }

        if (pePiece == null) {
            // we can get here without the piece actually being active when we have a very slow peer that is sent a request for the last
            // block of a piece, doesn't reply, the request gets cancelled, (and piece marked as inactive) and then it sends the block
            // and our 'recover block as useful' logic kicks in, writes the block, completes the piece, triggers a piece check and here we are

            return;
        }

        final ArrayList peer_manager_listeners = peer_manager_listeners_cow;

        for (int i = 0; i < peer_manager_listeners.size(); i++) {
            try {
                ((PEPeerManagerListener) peer_manager_listeners.get(i)).pieceRemoved(this, pePiece);

            } catch (Throwable e) {

                Debug.printStackTrace(e);
            }
        }
    }

    public int getNbActivePieces() {
        return nbPiecesActive;
    }

    public String getElapsedTime() {
        return TimeFormatter.format((SystemTime.getCurrentTime() - _timeStarted) / 1000);
    }

    // Returns time started in ms
    public long getTimeStarted(boolean mono) {
        return mono ? _timeStarted_mono : _timeStarted;
    }

    public long getTimeStartedSeeding(boolean mono) {
        return mono ? _timeStartedSeeding_mono : _timeStartedSeeding;
    }

    private byte[] computeMd5Hash(DirectByteBuffer buffer) {
        BrokenMd5Hasher md5 = new BrokenMd5Hasher();

        md5.reset();
        final int position = buffer.position(DirectByteBuffer.SS_DW);
        md5.update(buffer.getBuffer(DirectByteBuffer.SS_DW));
        buffer.position(DirectByteBuffer.SS_DW, position);
        ByteBuffer md5Result = ByteBuffer.allocate(16);
        md5Result.position(0);
        md5.finalDigest(md5Result);

        final byte[] result = new byte[16];
        md5Result.position(0);
        for (int i = 0; i < result.length; i++) {
            result[i] = md5Result.get();
        }

        return result;
    }

    private void MD5CheckPiece(PEPiece piece, boolean correct) {
        final String[] writers = piece.getWriters();
        int offset = 0;
        for (int i = 0; i < writers.length; i++) {
            final int length = piece.getBlockSize(i);
            final String peer = writers[i];
            if (peer != null) {
                DirectByteBuffer buffer = disk_mgr.readBlock(piece.getPieceNumber(), offset, length);

                if (buffer != null) {
                    final byte[] hash = computeMd5Hash(buffer);
                    buffer.returnToPool();
                    buffer = null;
                    piece.addWrite(i, peer, hash, correct);
                }
            }
            offset += length;
        }
    }

    public void checkCompleted(DiskManagerCheckRequest request, boolean passed) {
        if (TEST_PERIODIC_SEEDING_SCAN_FAIL_HANDLING && ((Integer) request.getUserData()).intValue() == CHECK_REASON_SEEDING_CHECK) {

            passed = false;
        }

        try {
            piece_check_result_list_mon.enter();

            piece_check_result_list.add(new Object[] { request, new Integer(passed ? 1 : 0) });
        } finally {
            piece_check_result_list_mon.exit();
        }
    }

    public void checkCancelled(DiskManagerCheckRequest request) {
        try {
            piece_check_result_list_mon.enter();

            piece_check_result_list.add(new Object[] { request, new Integer(2) });

        } finally {
            piece_check_result_list_mon.exit();
        }
    }

    public void checkFailed(DiskManagerCheckRequest request, Throwable cause) {
        try {
            piece_check_result_list_mon.enter();

            piece_check_result_list.add(new Object[] { request, new Integer(0) });

        } finally {
            piece_check_result_list_mon.exit();
        }
    }

    public boolean needsMD5CheckOnCompletion(int pieceNumber) {
        final PEPieceImpl piece = pePieces[pieceNumber];
        if (piece == null)
            return false;
        return piece.getPieceWrites().size() > 0;
    }

    private void processPieceCheckResult(DiskManagerCheckRequest request, int outcome) {
        final int check_type = ((Integer) request.getUserData()).intValue();

        try {

            final int pieceNumber = request.getPieceNumber();

            // System.out.println( "processPieceCheckResult(" + _finished + "/" + recheck_on_completion + "):" + pieceNumber +
            // "/" + piece + " - " + result );

            // passed = 1, failed = 0, cancelled = 2

            if (check_type == CHECK_REASON_COMPLETE) {
                // this is a recheck, so don't send HAVE msgs

                if (outcome == 0) {

                    // piece failed; restart the download afresh
                    Debug.out(getDisplayName() + ": Piece #" + pieceNumber + " failed final re-check. Re-downloading...");

                    if (!restart_initiated) {

                        restart_initiated = true;
                        adapter.restartDownload(true);
                    }
                }

                return;

            } else if (check_type == CHECK_REASON_SEEDING_CHECK || check_type == CHECK_REASON_BAD_PIECE_CHECK) {

                if (outcome == 0) {

                    if (check_type == CHECK_REASON_SEEDING_CHECK) {

                        Debug.out(getDisplayName() + "Piece #" + pieceNumber + " failed recheck while seeding. Re-downloading...");

                    } else {

                        Debug.out(getDisplayName() + "Piece #" + pieceNumber + " failed recheck after being reported as bad. Re-downloading...");
                    }

                    Logger.log(new LogAlert(this, LogAlert.REPEATABLE, LogAlert.AT_ERROR, "Download '" + getDisplayName() + "': piece "
                            + pieceNumber + " has been corrupted, re-downloading"));

                    if (!restart_initiated) {

                        restart_initiated = true;

                        adapter.restartDownload(true);
                    }
                }

                return;
            }

            // piece can be null when running a recheck on completion
            // actually, give the above code I don't think this is true anymore...

            final PEPieceImpl pePiece = pePieces[pieceNumber];

            if (outcome == 1 || is_metadata_download) {

                // the piece has been written correctly

                try {
                    if (pePiece != null) {

                        if (needsMD5CheckOnCompletion(pieceNumber)) {
                            MD5CheckPiece(pePiece, true);
                        }

                        final List list = pePiece.getPieceWrites();

                        if (list.size() > 0) {

                            // For each Block
                            for (int i = 0; i < pePiece.getNbBlocks(); i++) {

                                // System.out.println("Processing block " + i);
                                // Find out the correct hash
                                final List listPerBlock = pePiece.getPieceWrites(i);
                                byte[] correctHash = null;
                                // PEPeer correctSender = null;
                                Iterator iterPerBlock = listPerBlock.iterator();
                                while (iterPerBlock.hasNext()) {
                                    final PEPieceWriteImpl write = (PEPieceWriteImpl) iterPerBlock.next();
                                    if (write.isCorrect()) {
                                        correctHash = write.getHash();
                                        // correctSender = write.getSender();
                                    }
                                }
                                // System.out.println("Correct Hash " + correctHash);
                                // If it's found
                                if (correctHash != null) {
                                    iterPerBlock = listPerBlock.iterator();
                                    while (iterPerBlock.hasNext()) {
                                        final PEPieceWriteImpl write = (PEPieceWriteImpl) iterPerBlock.next();
                                        if (!Arrays.equals(write.getHash(), correctHash)) {
                                            // Bad peer found here
                                            badPeerDetected(write.getSender(), pieceNumber);
                                        }
                                    }
                                }
                            }
                        }
                    }
                } finally {
                    // regardless of any possible failure above, tidy up correctly

                    removePiece(pePiece, pieceNumber);

                    // send all clients a have message
                    sendHave(pieceNumber); // XXX: if Done isn't set yet, might refuse to send this piece
                }
            } else if (outcome == 0) {

                // the piece is corrupt

                if (pePiece != null) {

                    try {
                        MD5CheckPiece(pePiece, false);

                        final String[] writers = pePiece.getWriters();
                        final List uniqueWriters = new ArrayList();
                        final int[] writesPerWriter = new int[writers.length];
                        for (int i = 0; i < writers.length; i++) {
                            final String writer = writers[i];
                            if (writer != null) {
                                int writerId = uniqueWriters.indexOf(writer);
                                if (writerId == -1) {
                                    uniqueWriters.add(writer);
                                    writerId = uniqueWriters.size() - 1;
                                }
                                writesPerWriter[writerId]++;
                            }
                        }
                        final int nbWriters = uniqueWriters.size();
                        if (nbWriters == 1) {
                            // Very simple case, only 1 peer contributed for that piece,
                            // so, let's mark it as a bad peer

                            String bad_ip = (String) uniqueWriters.get(0);

                            PEPeerTransport bad_peer = getTransportFromAddress(bad_ip);

                            if (bad_peer != null) {

                                bad_peer.sendBadPiece(pieceNumber);
                            }

                            badPeerDetected(bad_ip, pieceNumber);

                            // and let's reset the whole piece
                            pePiece.reset();

                        } else if (nbWriters > 1) {

                            int maxWrites = 0;
                            String bestWriter = null;

                            PEPeerTransport bestWriter_transport = null;

                            for (int i = 0; i < uniqueWriters.size(); i++) {

                                final int writes = writesPerWriter[i];

                                if (writes > maxWrites) {

                                    final String writer = (String) uniqueWriters.get(i);

                                    PEPeerTransport pt = getTransportFromAddress(writer);

                                    if (pt != null && pt.getReservedPieceNumbers() == null
                                            && !ip_filter.isInRange(writer, getDisplayName(), getTorrentHash())) {

                                        bestWriter = writer;
                                        maxWrites = writes;

                                        bestWriter_transport = pt;
                                    }
                                }
                            }

                            if (bestWriter != null) {

                                pePiece.setReservedBy(bestWriter);

                                bestWriter_transport.addReservedPieceNumber(pePiece.getPieceNumber());

                                pePiece.setRequestable();

                                for (int i = 0; i < pePiece.getNbBlocks(); i++) {

                                    // If the block was contributed by someone else

                                    if (writers[i] == null || !writers[i].equals(bestWriter)) {

                                        pePiece.reDownloadBlock(i);
                                    }
                                }
                            } else {

                                // In all cases, reset the piece
                                pePiece.reset();
                            }
                        } else {

                            // In all cases, reset the piece
                            pePiece.reset();
                        }

                        // if we are in end-game mode, we need to re-add all the piece chunks
                        // to the list of chunks needing to be downloaded
                        piecePicker.addEndGameChunks(pePiece);
                        _stats.hashFailed(pePiece.getLength());

                    } catch (Throwable e) {

                        Debug.printStackTrace(e);

                        // anything craps out in the above code, ensure we tidy up

                        pePiece.reset();
                    }
                } else {

                    // no active piece for some reason, clear down DM piece anyway

                    Debug.out(getDisplayName() + "Piece #" + pieceNumber + " failed check and no active piece, resetting...");

                    dm_pieces[pieceNumber].reset();
                }
            } else {

                // cancelled, download stopped
            }
        } finally {

            if (check_type == CHECK_REASON_SCAN) {
                rescan_piece_time = SystemTime.getCurrentTime();
            }

            if (!seeding_mode) {
                checkFinished(false);
            }
        }
    }

    private void badPeerDetected(String ip, int piece_number) {
        boolean hash_fail = piece_number >= 0;

        // note that peer can be NULL but things still work in the main

        PEPeerTransport peer = getTransportFromAddress(ip);

        if (hash_fail && peer != null) {

            Iterator<PEPeerManagerListener> it = peer_manager_listeners_cow.iterator();

            while (it.hasNext()) {

                try {
                    it.next().peerSentBadData(this, peer, piece_number);

                } catch (Throwable e) {

                    Debug.printStackTrace(e);
                }
            }
        }
        // Debug.out("Bad Peer Detected: " + peerIP + " [" + peer.getClient() + "]");

        IpFilterManager filter_manager = IpFilterManagerFactory.getSingleton();

        // Ban fist to avoid a fast reco of the bad peer

        int nbWarnings = filter_manager.getBadIps().addWarningForIp(ip);

        boolean disconnect_peer = false;

        // no need to reset the bad chunk count as the peer is going to be disconnected and
        // if it comes back it'll start afresh

        // warning limit only applies to hash-fails, discards cause immediate action

        if (nbWarnings > WARNINGS_LIMIT) {

            if (COConfigurationManager.getBooleanParameter("Ip Filter Enable Banning")) {

                // if a block-ban occurred, check other connections

                if (ip_filter.ban(ip, getDisplayName(), false)) {

                    checkForBannedConnections();
                }

                // Trace the ban
                if (Logger.isEnabled()) {
                    Logger.log(new LogEvent(peer, LOGID, LogEvent.LT_ERROR, ip + " : has been banned and won't be able "
                            + "to connect until you restart azureus"));
                }

                disconnect_peer = true;
            }
        } else if (!hash_fail) {

            // for failures due to excessive discard we boot the peer anyway

            disconnect_peer = true;

        }

        if (disconnect_peer) {

            if (peer != null) {

                final int ps = peer.getPeerState();

                // might have been through here very recently and already started closing
                // the peer (due to multiple bad blocks being found from same peer when checking piece)
                if (!(ps == PEPeer.CLOSING || ps == PEPeer.DISCONNECTED)) {
                    // Close connection
                    closeAndRemovePeer(peer, "has sent too many " + (hash_fail ? "bad pieces" : "discarded blocks") + ", " + WARNINGS_LIMIT
                            + " max.", true);
                }
            }
        }
    }

    public PEPiece[] getPieces() {
        return pePieces;
    }

    public PEPiece getPiece(int pieceNumber) {
        return pePieces[pieceNumber];
    }

    public PEPeerStats createPeerStats(PEPeer owner) {
        return (new PEPeerStatsImpl(owner));
    }

    public DiskManagerReadRequest createDiskManagerRequest(int pieceNumber, int offset, int length) {
        return (disk_mgr.createReadRequest(pieceNumber, offset, length));
    }

    public boolean requestExists(String peer_ip, int piece_number, int offset, int length) {
        ArrayList peer_transports = peer_transports_cow;

        DiskManagerReadRequest request = null;

        for (int i = 0; i < peer_transports.size(); i++) {

            PEPeerTransport conn = (PEPeerTransport) peer_transports.get(i);

            if (conn.getIp().equals(peer_ip)) {

                if (request == null) {

                    request = createDiskManagerRequest(piece_number, offset, length);
                }

                if (conn.getRequestIndex(request) != -1) {

                    return (true);
                }
            }
        }

        return (false);
    }

    public boolean seedPieceRecheck() {
        if (!(enable_seeding_piece_rechecks || isSeeding())) {

            return (false);
        }

        int max_reads = 0;
        int max_reads_index = 0;

        if (TEST_PERIODIC_SEEDING_SCAN_FAIL_HANDLING) {

            max_reads_index = (int) (Math.random() * dm_pieces.length);
            ;
            max_reads = dm_pieces[max_reads_index].getNbBlocks() * 3;

        } else {

            for (int i = 0; i < dm_pieces.length; i++) {

                // skip dnd pieces

                DiskManagerPiece dm_piece = dm_pieces[i];

                if (!dm_piece.isDone()) {

                    continue;
                }

                int num = dm_piece.getReadCount() & 0xffff;

                if (num > SEED_CHECK_WAIT_MARKER) {

                    // recently been checked, skip for a while

                    num--;

                    if (num == SEED_CHECK_WAIT_MARKER) {

                        num = 0;
                    }

                    dm_piece.setReadCount((short) num);

                } else {

                    if (num > max_reads) {

                        max_reads = num;
                        max_reads_index = i;
                    }
                }
            }
        }

        if (max_reads > 0) {

            DiskManagerPiece dm_piece = dm_pieces[max_reads_index];

            // if the piece has been read 3 times (well, assuming each block is read once,
            // which is obviously wrong, but...)

            if (max_reads >= dm_piece.getNbBlocks() * 3) {

                DiskManagerCheckRequest req = disk_mgr.createCheckRequest(max_reads_index, new Integer(CHECK_REASON_SEEDING_CHECK));

                req.setAdHoc(true);

                req.setLowPriority(true);

                if (Logger.isEnabled())
                    Logger.log(new LogEvent(disk_mgr.getTorrent(), LOGID, "Rechecking piece " + max_reads_index + " while seeding as most active"));

                disk_mgr.enqueueCheckRequest(req, this);

                dm_piece.setReadCount((short) 65535);

                // clear out existing, non delayed pieces so we start counting piece activity
                // again

                for (int i = 0; i < dm_pieces.length; i++) {

                    if (i != max_reads_index) {

                        int num = dm_pieces[i].getReadCount() & 0xffff;

                        if (num < SEED_CHECK_WAIT_MARKER) {

                            dm_pieces[i].setReadCount((short) 0);
                        }
                    }
                }

                return (true);
            }
        }

        return (false);
    }

    public void addListener(PEPeerManagerListener l) {
        try {
            this_mon.enter();

            // copy on write
            final ArrayList peer_manager_listeners = new ArrayList(peer_manager_listeners_cow.size() + 1);
            peer_manager_listeners.addAll(peer_manager_listeners_cow);
            peer_manager_listeners.add(l);
            peer_manager_listeners_cow = peer_manager_listeners;

        } finally {

            this_mon.exit();
        }
    }

    public void removeListener(PEPeerManagerListener l) {
        try {
            this_mon.enter();

            // copy on write
            final ArrayList peer_manager_listeners = new ArrayList(peer_manager_listeners_cow);
            peer_manager_listeners.remove(l);
            peer_manager_listeners_cow = peer_manager_listeners;

        } finally {

            this_mon.exit();
        }
    }

    private void checkForBannedConnections() {
        if (ip_filter.isEnabled()) { // if ipfiltering is enabled, remove any existing filtered connections
            ArrayList to_close = null;

            final ArrayList peer_transports = peer_transports_cow;

            String name = getDisplayName();
            byte[] hash = getTorrentHash();

            for (int i = 0; i < peer_transports.size(); i++) {
                final PEPeerTransport conn = (PEPeerTransport) peer_transports.get(i);

                if (ip_filter.isInRange(conn.getIp(), name, hash)) {
                    if (to_close == null)
                        to_close = new ArrayList();
                    to_close.add(conn);
                }
            }

            if (to_close != null) {
                for (int i = 0; i < to_close.size(); i++) {
                    closeAndRemovePeer((PEPeerTransport) to_close.get(i), "IPFilter banned IP address", true);
                }
            }
        }
    }

    public boolean isSeeding() {
        return (seeding_mode);
    }

    public boolean isMetadataDownload() {
        return (is_metadata_download);
    }

    public int getTorrentInfoDictSize() {
        return (metadata_infodict_size);
    }

    public void setTorrentInfoDictSize(int size) {
        metadata_infodict_size = size;
    }

    public boolean isInEndGameMode() {
        return piecePicker.isInEndGameMode();
    }

    public boolean isSuperSeedMode() {
        return superSeedMode;
    }

    public boolean canToggleSuperSeedMode() {
        if (superSeedMode) {

            return (true);
        }

        return (superSeedPieces == null && this.getRemaining() == 0);
    }

    public void setSuperSeedMode(boolean _superSeedMode) {
        if (_superSeedMode == superSeedMode) {

            return;
        }

        boolean kick_peers = false;

        if (_superSeedMode) {

            if (superSeedPieces == null && this.getRemaining() == 0) {

                superSeedMode = true;

                initialiseSuperSeedMode();

                kick_peers = true;
            }
        } else {

            superSeedMode = false;

            kick_peers = true;
        }

        if (kick_peers) {

            // turning on/off super-seeding, gotta kick all connected peers so they get the
            // "right" bitfield

            ArrayList peer_transports = peer_transports_cow;

            for (int i = 0; i < peer_transports.size(); i++) {

                PEPeerTransport conn = (PEPeerTransport) peer_transports.get(i);

                closeAndRemovePeer(conn, "Turning on super-seeding", false);
            }
        }
    }

    private void initialiseSuperSeedMode() {
        superSeedPieces = new SuperSeedPiece[_nbPieces];
        for (int i = 0; i < _nbPieces; i++) {
            superSeedPieces[i] = new SuperSeedPiece(this, i);
        }
    }

    private void updatePeersInSuperSeedMode() {
        if (!superSeedMode) {
            return;
        }

        // Refresh the update time in case this is needed
        for (int i = 0; i < superSeedPieces.length; i++) {
            superSeedPieces[i].updateTime();
        }

        // Use the same number of announces than unchoke
        int nbUnchoke = adapter.getMaxUploads();
        if (superSeedModeNumberOfAnnounces >= 2 * nbUnchoke)
            return;

        // Find an available Peer
        PEPeer selectedPeer = null;
        List sortedPeers = null;

        final ArrayList peer_transports = peer_transports_cow;

        sortedPeers = new ArrayList(peer_transports.size());
        Iterator iter = peer_transports.iterator();
        while (iter.hasNext()) {
            sortedPeers.add(new SuperSeedPeer((PEPeer) iter.next()));
        }

        Collections.sort(sortedPeers);
        iter = sortedPeers.iterator();
        while (iter.hasNext()) {
            final PEPeer peer = ((SuperSeedPeer) iter.next()).peer;
            if ((peer.getUniqueAnnounce() == -1) && (peer.getPeerState() == PEPeer.TRANSFERING)) {
                selectedPeer = peer;
                break;
            }
        }

        if (selectedPeer == null || selectedPeer.getPeerState() >= PEPeer.CLOSING)
            return;

        if (selectedPeer.getUploadHint() == 0) {
            // Set to infinite
            selectedPeer.setUploadHint(Constants.CRAPPY_INFINITY_AS_INT);
        }

        // Find a piece
        boolean found = false;
        SuperSeedPiece piece = null;
        boolean loopdone = false; // add loop status

        while (!found) {
            piece = superSeedPieces[superSeedModeCurrentPiece];
            if (piece.getLevel() > 0) {
                piece = null;
                superSeedModeCurrentPiece++;
                if (superSeedModeCurrentPiece >= _nbPieces) {
                    superSeedModeCurrentPiece = 0;

                    if (loopdone) { // if already been here, has been full loop through pieces, quit
                        // quit superseed mode
                        superSeedMode = false;
                        closeAndRemoveAllPeers("quiting SuperSeed mode", true);
                        return;
                    } else {
                        // loopdone==false --> first time here --> go through the pieces
                        // for a second time to check if reserved pieces have got freed due to peers leaving
                        loopdone = true;
                    }
                }
            } else {
                found = true;
            }
        }

        if (piece == null) {
            return;
        }

        // If this peer already has this piece, return (shouldn't happen)
        if (selectedPeer.isPieceAvailable(piece.getPieceNumber())) {
            return;
        }

        selectedPeer.setUniqueAnnounce(piece.getPieceNumber());
        superSeedModeNumberOfAnnounces++;
        piece.pieceRevealedToPeer();
        ((PEPeerTransport) selectedPeer).sendHave(piece.getPieceNumber());
    }

    public void updateSuperSeedPiece(PEPeer peer, int pieceNumber) {
        // currently this gets only called from bitfield scan function in PEPeerTransportProtocol
        if (!superSeedMode)
            return;
        superSeedPieces[pieceNumber].peerHasPiece(null);
        if (peer.getUniqueAnnounce() == pieceNumber) {
            peer.setUniqueAnnounce(-1);
            superSeedModeNumberOfAnnounces--;
        }
    }

    public boolean isPrivateTorrent() {
        return (is_private_torrent);
    }

    public boolean isExtendedMessagingEnabled() {
        return (adapter.isExtendedMessagingEnabled());
    }

    public boolean isPeerExchangeEnabled() {
        return (adapter.isPeerExchangeEnabled());
    }

    public LimitedRateGroup getUploadLimitedRateGroup() {
        return upload_limited_rate_group;
    }

    public LimitedRateGroup getDownloadLimitedRateGroup() {
        return download_limited_rate_group;
    }

    /** To retreive arbitrary objects against this object. */
    public Object getData(String key) {
        try {
            this_mon.enter();

            if (user_data == null)
                return null;

            return user_data.get(key);

        } finally {

            this_mon.exit();
        }
    }

    /** To store arbitrary objects against a control. */

    public void setData(String key, Object value) {
        try {
            this_mon.enter();

            if (user_data == null) {
                user_data = new HashMap();
            }
            if (value == null) {
                if (user_data.containsKey(key))
                    user_data.remove(key);
            } else {
                user_data.put(key, value);
            }
        } finally {
            this_mon.exit();
        }
    }

    public int getConnectTimeout(int ct_def) {
        if (ct_def <= 0) {

            return (ct_def);
        }

        if (seeding_mode) {

            // seeding mode connections are already de-prioritised so nothing to do

            return (ct_def);
        }

        int max_sim_con = TCPConnectionManager.MAX_SIMULTANIOUS_CONNECT_ATTEMPTS;

        // high, let's not mess with things

        if (max_sim_con >= 50) {

            return (ct_def);
        }

        // we have somewhat limited outbound connection limits, see if it makes sense to
        // reduce the connect timeout to prevent connection stall due to a bunch getting
        // stuck 'connecting' for a long time and stalling us

        int connected = _seeds + _peers;
        int connecting = _tcpConnectingConnections;
        int queued = _tcpPendingConnections;

        int not_yet_connected = peer_database.getDiscoveredPeerCount();

        int max = getMaxConnections();

        int potential = connecting + queued + not_yet_connected;

        /*
         * System.out.println( "connected=" + connected + ", queued=" + queued + ", connecting=" + connecting + ", queued=" + queued + ", not_yet=" +
         * not_yet_connected + ", max=" + max );
         */

        // not many peers -> don't amend

        int lower_limit = max / 4;

        if (potential <= lower_limit || max == lower_limit) {

            return (ct_def);
        }

        // if we got lots of potential, use minimum delay

        final int MIN_CT = 7500;

        if (potential >= max) {

            return (MIN_CT);
        }

        // scale between MIN and ct_def

        int pos = potential - lower_limit;
        int scale = max - lower_limit;

        int res = MIN_CT + (ct_def - MIN_CT) * (scale - pos) / scale;

        // System.out.println( "scaled->" + res );

        return (res);
    }

    private void doConnectionChecks() {
        // every 1 second
        if (mainloop_loop_count % MAINLOOP_ONE_SECOND_INTERVAL == 0) {
            final ArrayList peer_transports = peer_transports_cow;

            int num_waiting_establishments = 0;

            int udp_connections = 0;

            for (int i = 0; i < peer_transports.size(); i++) {
                final PEPeerTransport transport = (PEPeerTransport) peer_transports.get(i);

                // update waiting count
                final int state = transport.getConnectionState();
                if (state == PEPeerTransport.CONNECTION_PENDING || state == PEPeerTransport.CONNECTION_CONNECTING) {
                    num_waiting_establishments++;
                }

                if (!transport.isTCP()) {

                    udp_connections++;
                }
            }

            int allowed_seeds = getMaxSeedConnections();

            if (allowed_seeds > 0) {

                int to_disconnect = _seeds - allowed_seeds;

                if (to_disconnect > 0) {

                    // seeds are limited by people trying to get a reasonable upload by connecting
                    // to leechers where possible. disconnect seeds from end of list to prevent
                    // cycling of seeds

                    for (int i = peer_transports.size() - 1; i >= 0 && to_disconnect > 0; i--) {

                        final PEPeerTransport transport = (PEPeerTransport) peer_transports.get(i);

                        if (transport.isSeed()) {

                            closeAndRemovePeer(transport, "Too many seeds", false);

                            to_disconnect--;
                        }
                    }
                }
            }

            // pass from storage to connector
            int allowed = getMaxNewConnectionsAllowed();

            if (allowed < 0 || allowed > 1000)
                allowed = 1000; // ensure a very upper limit so it doesnt get out of control when using PEX

            if (adapter.isNATHealthy()) { // if unfirewalled, leave slots avail for remote connections
                final int free = getMaxConnections() / 20; // leave 5%
                allowed = allowed - free;
            }

            if (allowed > 0) {
                // try and connect only as many as necessary

                final int wanted = TCPConnectionManager.MAX_SIMULTANIOUS_CONNECT_ATTEMPTS - num_waiting_establishments;

                if (wanted > allowed) {
                    num_waiting_establishments += wanted - allowed;
                }

                int remaining = allowed;

                int tcp_remaining = TCPNetworkManager.getSingleton().getConnectDisconnectManager().getMaxOutboundPermitted();

                int udp_remaining = UDPNetworkManager.getSingleton().getConnectionManager().getMaxOutboundPermitted();

                // load stored peer-infos to be established
                while (num_waiting_establishments < TCPConnectionManager.MAX_SIMULTANIOUS_CONNECT_ATTEMPTS
                        && (tcp_remaining > 0 || udp_remaining > 0)) {
                    if (!is_running)
                        break;

                    final PeerItem item = peer_database.getNextOptimisticConnectPeer();

                    if (item == null || !is_running)
                        break;

                    final PeerItem self = peer_database.getSelfPeer();
                    if (self != null && self.equals(item)) {
                        continue;
                    }

                    if (!isAlreadyConnected(item)) {
                        final String source = PeerItem.convertSourceString(item.getSource());

                        final boolean use_crypto = item.getHandshakeType() == PeerItemFactory.HANDSHAKE_TYPE_CRYPTO;

                        int tcp_port = item.getTCPPort();
                        int udp_port = item.getUDPPort();

                        if (udp_port == 0 && udp_probe_enabled) {

                            // for probing we assume udp port same as tcp

                            udp_port = tcp_port;
                        }

                        boolean prefer_udp_overall = prefer_udp || prefer_udp_default;

                        if (prefer_udp_overall && udp_port == 0) {

                            // see if we have previous record of this address as udp connectable

                            byte[] address = item.getIP().getBytes();

                            BloomFilter bloom = prefer_udp_bloom;

                            if (bloom != null && bloom.contains(address)) {

                                udp_port = tcp_port;
                            }
                        }

                        boolean tcp_ok = TCPNetworkManager.TCP_OUTGOING_ENABLED && tcp_port > 0 && tcp_remaining > 0;
                        boolean udp_ok = UDPNetworkManager.UDP_OUTGOING_ENABLED && udp_port > 0 && udp_remaining > 0;

                        if (tcp_ok && !(prefer_udp_overall && udp_ok)) {

                            if (makeNewOutgoingConnection(source, item.getAddressString(), tcp_port, udp_port, true, use_crypto, item
                                    .getCryptoLevel(), null) == null) {

                                tcp_remaining--;

                                num_waiting_establishments++;
                                remaining--;
                            }
                        } else if (udp_ok) {

                            if (makeNewOutgoingConnection(source, item.getAddressString(), tcp_port, udp_port, false, use_crypto, item
                                    .getCryptoLevel(), null) == null) {

                                udp_remaining--;

                                num_waiting_establishments++;

                                remaining--;
                            }
                        }
                    }
                }

                if (UDPNetworkManager.UDP_OUTGOING_ENABLED && remaining > 0 && udp_remaining > 0 && udp_connections < MAX_UDP_CONNECTIONS) {

                    doUDPConnectionChecks(remaining);
                }
            }
        }

        // every 5 seconds
        if (mainloop_loop_count % MAINLOOP_FIVE_SECOND_INTERVAL == 0) {
            final ArrayList peer_transports = peer_transports_cow;

            for (int i = 0; i < peer_transports.size(); i++) {
                final PEPeerTransport transport = (PEPeerTransport) peer_transports.get(i);

                // check for timeouts
                if (transport.doTimeoutChecks())
                    continue;

                // keep-alive check
                transport.doKeepAliveCheck();

                // speed tuning check
                transport.doPerformanceTuningCheck();
            }
        }

        // every 10 seconds check for connected + banned peers
        if (mainloop_loop_count % MAINLOOP_TEN_SECOND_INTERVAL == 0) {
            final long last_update = ip_filter.getLastUpdateTime();
            if (last_update != ip_filter_last_update_time) {
                ip_filter_last_update_time = last_update;
                checkForBannedConnections();
            }
        }

        // every 30 seconds
        if (mainloop_loop_count % MAINLOOP_THIRTY_SECOND_INTERVAL == 0) {
            // if we're at our connection limit, time out the least-useful
            // one so we can establish a possibly-better new connection
            optimisticDisconnectCount = 0;
            if (getMaxNewConnectionsAllowed() == 0) { // we've reached limit
                doOptimisticDisconnect(false, false);
            }
        }

        // sweep over all peers in a 60 second timespan
        float percentage = ((mainloop_loop_count % MAINLOOP_SIXTY_SECOND_INTERVAL) + 1F) / (1F * MAINLOOP_SIXTY_SECOND_INTERVAL);
        int goal;
        if (mainloop_loop_count % MAINLOOP_SIXTY_SECOND_INTERVAL == 0) {
            goal = 0;
            sweepList = peer_transports_cow;
        } else
            goal = (int) Math.floor(percentage * sweepList.size());

        for (int i = nextPEXSweepIndex; i < goal && i < sweepList.size(); i++) {
            // System.out.println(mainloop_loop_count+" %:"+percentage+" start:"+nextPEXSweepIndex+" current:"+i+" <"+goal+"/"+sweepList.size());
            final PEPeerTransport peer = (PEPeerTransport) sweepList.get(i);
            peer.updatePeerExchange();
        }

        nextPEXSweepIndex = goal;
    }

    private void doUDPConnectionChecks(int number) {
        List new_connections = null;

        try {
            peer_transports_mon.enter();

            long now = SystemTime.getCurrentTime();

            if (udp_reconnects.size() > 0 && now - last_udp_reconnect >= UDP_RECONNECT_MIN_MILLIS) {

                last_udp_reconnect = now;

                Iterator it = udp_reconnects.values().iterator();

                PEPeerTransport peer = (PEPeerTransport) it.next();

                it.remove();

                if (Logger.isEnabled()) {
                    Logger.log(new LogEvent(this, LOGID, LogEvent.LT_INFORMATION, "Reconnecting to previous failed peer "
                            + peer.getPeerItemIdentity().getAddressString()));
                }

                if (new_connections == null) {

                    new_connections = new ArrayList();
                }

                new_connections.add(peer);

                number--;

                if (number <= 0) {

                    return;
                }
            }

            if (pending_nat_traversals.size() == 0) {

                return;
            }

            int max = MAX_UDP_TRAVERSAL_COUNT;

            // bigger the swarm, less chance of doing it

            if (seeding_mode) {

                if (_peers > 8) {

                    max = 0;

                } else {

                    max = 1;
                }
            } else if (_seeds > 8) {

                max = 0;

            } else if (_seeds > 4) {

                max = 1;
            }

            int avail = max - udp_traversal_count;

            int to_do = Math.min(number, avail);

            Iterator it = pending_nat_traversals.values().iterator();

            while (to_do > 0 && it.hasNext()) {

                final PEPeerTransport peer = (PEPeerTransport) it.next();

                it.remove();

                String peer_ip = peer.getPeerItemIdentity().getAddressString();

                if (AENetworkClassifier.categoriseAddress(peer_ip) != AENetworkClassifier.AT_PUBLIC) {

                    continue;
                }

                to_do--;

                PeerNATTraverser.getSingleton().create(this, new InetSocketAddress(peer_ip, peer.getPeerItemIdentity().getUDPPort()),
                        new PeerNATTraversalAdapter() {
                            private boolean done;

                            public void success(InetSocketAddress target) {
                                complete();

                                PEPeerTransport newTransport = peer.reconnect(true, false);

                                if (newTransport != null) {

                                    newTransport.setData(PEER_NAT_TRAVERSE_DONE_KEY, "");
                                }
                            }

                            public void failed() {
                                complete();
                            }

                            protected void complete() {
                                try {
                                    peer_transports_mon.enter();

                                    if (!done) {

                                        done = true;

                                        udp_traversal_count--;
                                    }
                                } finally {

                                    peer_transports_mon.exit();
                                }
                            }
                        });

                udp_traversal_count++;
            }
        } finally {

            peer_transports_mon.exit();

            if (new_connections != null) {

                for (int i = 0; i < new_connections.size(); i++) {

                    PEPeerTransport peer_item = (PEPeerTransport) new_connections.get(i);

                    // don't call when holding monitor - deadlock potential
                    peer_item.reconnect(true, false);

                }
            }
        }
    }

    // counter is reset every 30s by doConnectionChecks()
    private int optimisticDisconnectCount = 0;

    public boolean doOptimisticDisconnect(boolean pending_lan_local_peer, boolean force) {

        final ArrayList peer_transports = peer_transports_cow;
        PEPeerTransport max_transport = null;
        PEPeerTransport max_seed_transport = null;
        PEPeerTransport max_non_lan_transport = null;

        long max_time = 0;
        long max_seed_time = 0;
        long max_non_lan_time = 0;

        List<Long> activeConnectionTimes = new ArrayList<Long>(peer_transports.size());

        int lan_peer_count = 0;

        for (int i = 0; i < peer_transports.size(); i++) {
            final PEPeerTransport peer = (PEPeerTransport) peer_transports.get(i);

            if (peer.getConnectionState() == PEPeerTransport.CONNECTION_FULLY_ESTABLISHED) {

                final long timeSinceConnection = peer.getTimeSinceConnectionEstablished();
                final long timeSinceSentData = peer.getTimeSinceLastDataMessageSent();

                activeConnectionTimes.add(timeSinceConnection);

                long peerTestTime = 0;
                if (seeding_mode) {
                    if (timeSinceSentData != -1)
                        peerTestTime = timeSinceSentData; // ensure we've sent them at least one data message to qualify for drop
                } else {
                    final long timeSinceGoodData = peer.getTimeSinceGoodDataReceived();

                    if (timeSinceGoodData == -1)
                        peerTestTime += timeSinceConnection; // never received
                    else
                        peerTestTime += timeSinceGoodData;

                    // try to drop unInteresting in favor of Interesting connections
                    if (!peer.isInteresting()) {
                        if (!peer.isInterested()) // if mutually unInterested, really try to drop the connection
                            peerTestTime += timeSinceConnection + timeSinceSentData; // if we never sent, it will subtract 1, which is good
                        else
                            peerTestTime += (timeSinceConnection - timeSinceSentData); // try to give interested peers a chance to get data

                        peerTestTime *= 2;
                    }

                    peerTestTime += peer.getSnubbedTime();
                }

                if (!peer.isIncoming()) {
                    peerTestTime = peerTestTime * 2; // prefer to drop a local connection, to make room for more remotes
                }

                if (peer.isLANLocal()) {

                    lan_peer_count++;

                } else {

                    if (peerTestTime > max_non_lan_time) {
                        max_non_lan_time = peerTestTime;
                        max_non_lan_transport = peer;
                    }
                }

                // anti-leech checks
                if (!seeding_mode) {

                    // remove long-term snubbed peers with higher probability
                    peerTestTime += peer.getSnubbedTime();
                    if (peer.getSnubbedTime() > 2 * 60) {
                        peerTestTime *= 1.5;
                    }

                    PEPeerStats pestats = peer.getStats();
                    // everybody has deserverd a chance of half an MB transferred data
                    if (pestats.getTotalDataBytesReceived() + pestats.getTotalDataBytesSent() > 1024 * 512) {
                        boolean goodPeer = true;

                        // we don't like snubbed peers with a negative gain
                        if (peer.isSnubbed() && pestats.getTotalDataBytesReceived() < pestats.getTotalDataBytesSent()) {
                            peerTestTime *= 1.5;
                            goodPeer = false;
                        }
                        // we don't like peers with a very bad ratio (10:1)
                        if (pestats.getTotalDataBytesSent() > pestats.getTotalDataBytesReceived() * 10) {
                            peerTestTime *= 2;
                            goodPeer = false;
                        }
                        // modify based on discarded : overall downloaded ratio
                        if (pestats.getTotalDataBytesReceived() > 0 && pestats.getTotalBytesDiscarded() > 0) {
                            peerTestTime =
                                    (long) (peerTestTime * (1.0 + ((double) pestats.getTotalBytesDiscarded() / (double) pestats
                                            .getTotalDataBytesReceived())));
                        }

                        // prefer peers that do some work, let the churn happen with peers that did nothing
                        if (goodPeer)
                            peerTestTime *= 0.7;
                    }
                }

                if (peerTestTime > max_time) {
                    max_time = peerTestTime;
                    max_transport = peer;
                }

                if (peer.isSeed() || peer.isRelativeSeed()) {

                    if (peerTestTime > max_seed_time) {
                        max_seed_time = peerTestTime;
                        max_seed_transport = peer;
                    }
                }
            }
        }

        long medianConnectionTime;

        if (activeConnectionTimes.size() > 0) {
            Collections.sort(activeConnectionTimes);
            medianConnectionTime = activeConnectionTimes.get(activeConnectionTimes.size() / 2);
        } else {
            medianConnectionTime = 0;
        }

        // allow 1 disconnect every 30s per 30 peers; 2 at least every 30s
        int maxOptimistics = Math.max(getMaxConnections() / 30, 2);

        // avoid unnecessary churn, e.g.
        if (!pending_lan_local_peer && !force && optimisticDisconnectCount >= maxOptimistics && medianConnectionTime < 5 * 60 * 1000)
            return false;

        // don't boot lan peers if we can help it (unless we have a few of them)

        if (max_transport != null) {

            final int LAN_PEER_MAX = 4;

            if (max_transport.isLANLocal() && lan_peer_count < LAN_PEER_MAX && max_non_lan_transport != null) {

                // override lan local max with non-lan local max

                max_transport = max_non_lan_transport;
                max_time = max_non_lan_time;
            }

            // if we have a seed limit, kick seeds in preference to non-seeds

            if (getMaxSeedConnections() > 0 && max_seed_transport != null && max_time > 5 * 60 * 1000) {
                closeAndRemovePeer(max_seed_transport, "timed out by doOptimisticDisconnect()", true);
                optimisticDisconnectCount++;
                return true;
            }

            if (max_transport != null && max_time > 5 * 60 * 1000) { // ensure a 5 min minimum test time
                closeAndRemovePeer(max_transport, "timed out by doOptimisticDisconnect()", true);
                optimisticDisconnectCount++;
                return true;
            }

            // kick worst peers to accomodate lan peer

            if (pending_lan_local_peer && lan_peer_count < LAN_PEER_MAX) {
                closeAndRemovePeer(max_transport, "making space for LAN peer in doOptimisticDisconnect()", true);
                optimisticDisconnectCount++;
                return true;
            }

            if (force) {

                closeAndRemovePeer(max_transport, "force removal of worst peer in doOptimisticDisconnect()", true);

                return true;
            }
        } else if (force) {

            if (peer_transports.size() > 0) {

                PEPeerTransport pt = (PEPeerTransport) peer_transports.get(new Random().nextInt(peer_transports.size()));

                closeAndRemovePeer(pt, "force removal of random peer in doOptimisticDisconnect()", true);

                return true;
            }
        }

        return false;
    }

    public PeerExchangerItem createPeerExchangeConnection(final PEPeerTransport base_peer) {
        if (base_peer.getTCPListenPort() > 0) { // only accept peers whose remote port is known
            final PeerItem peer =
                    PeerItemFactory.createPeerItem(base_peer.getIp(), base_peer.getTCPListenPort(), PeerItemFactory.PEER_SOURCE_PEER_EXCHANGE,
                            base_peer.getPeerItemIdentity().getHandshakeType(), base_peer.getUDPListenPort(), PeerItemFactory.CRYPTO_LEVEL_1, 0);

            return peer_database.registerPeerConnection(peer, new PeerExchangerItem.Helper() {
                public boolean isSeed() {
                    return base_peer.isSeed();
                }
            });
        }

        return null;
    }

    private boolean isAlreadyConnected(PeerItem peer_id) {
        final ArrayList peer_transports = peer_transports_cow;
        for (int i = 0; i < peer_transports.size(); i++) {
            final PEPeerTransport peer = (PEPeerTransport) peer_transports.get(i);
            if (peer.getPeerItemIdentity().equals(peer_id))
                return true;
        }
        return false;
    }

    public void peerVerifiedAsSelf(PEPeerTransport self) {
        if (self.getTCPListenPort() > 0) { // only accept self if remote port is known
            final PeerItem peer =
                    PeerItemFactory.createPeerItem(self.getIp(), self.getTCPListenPort(), PeerItem.convertSourceID(self.getPeerSource()), self
                            .getPeerItemIdentity().getHandshakeType(), self.getUDPListenPort(), PeerItemFactory.CRYPTO_LEVEL_CURRENT, 0);
            peer_database.setSelfPeer(peer);
        }
    }

    public void IPFilterEnabledChanged(boolean is_enabled) {
        if (is_enabled) {

            checkForBannedConnections();
        }
    }

    public boolean canIPBeBanned(String ip) {
        return true;
    }

    public boolean canIPBeBlocked(String ip, byte[] torrent_hash) {
        return true;
    }

    public void IPBlockedListChanged(IpFilter filter) {
        Iterator it = peer_transports_cow.iterator();

        String name = getDisplayName();
        byte[] hash = getTorrentHash();

        while (it.hasNext()) {
            try {
                PEPeerTransport peer = (PEPeerTransport) it.next();

                if (filter.isInRange(peer.getIp(), name, hash)) {
                    peer.closeConnection("IP address blocked by filters");
                }
            } catch (Exception e) {
            }
        }
    }

    public void IPBanned(BannedIp ip) {
        for (int i = 0; i < _nbPieces; i++) {
            if (pePieces[i] != null)
                pePieces[i].reDownloadBlocks(ip.getIp());
        }
    }

    public long getHiddenBytes() {
        if (hidden_piece < 0) {

            return (0);
        }

        return (dm_pieces[hidden_piece].getLength());
    }

    public int getHiddenPiece() {
        return (hidden_piece);
    }

    public int getUploadPriority() {
        return (adapter.getUploadPriority());
    }

    public int getAverageCompletionInThousandNotation() {
        final ArrayList peer_transports = peer_transports_cow;

        if (peer_transports != null) {
            final long total = disk_mgr.getTotalLength();

            final int my_completion = total == 0 ? 1000 : (int) ((1000 * (total - disk_mgr.getRemainingExcludingDND())) / total);

            int sum = my_completion == 1000 ? 0 : my_completion; // add in our own percentage if not seeding
            int num = my_completion == 1000 ? 0 : 1;

            for (int i = 0; i < peer_transports.size(); i++) {
                final PEPeer peer = (PEPeer) peer_transports.get(i);

                if (peer.getPeerState() == PEPeer.TRANSFERING && !peer.isSeed()) {
                    num++;
                    sum += peer.getPercentDoneInThousandNotation();
                }
            }

            return num > 0 ? sum / num : 0;
        }

        return -1;
    }

    public int getMaxConnections() {
        return (adapter.getMaxConnections());
    }

    public int getMaxSeedConnections() {
        return (adapter.getMaxSeedConnections());
    }

    public int getMaxNewConnectionsAllowed() {
        final int dl_max = getMaxConnections();

        final int allowed_peers = PeerUtils.numNewConnectionsAllowed(getPeerIdentityDataID(), dl_max);

        return (allowed_peers);
    }

    public int getSchedulePriority() {
        return isSeeding() ? Integer.MAX_VALUE : adapter.getPosition();
    }

    public boolean hasPotentialConnections() {
        return (pending_nat_traversals.size() + peer_database.getDiscoveredPeerCount() > 0);
    }

    public String getRelationText() {
        return (adapter.getLogRelation().getRelationText());
    }

    public Object[] getQueryableInterfaces() {
        return (adapter.getLogRelation().getQueryableInterfaces());
    }

    public PEPeerTransport getTransportFromIdentity(byte[] peer_id) {
        final ArrayList peer_transports = peer_transports_cow;
        for (int i = 0; i < peer_transports.size(); i++) {
            final PEPeerTransport conn = (PEPeerTransport) peer_transports.get(i);
            if (Arrays.equals(peer_id, conn.getId()))
                return conn;
        }
        return null;
    }

    /*
     * peer item is not reliable for general use public PEPeerTransport getTransportFromPeerItem(PeerItem peerItem) { ArrayList peer_transports
     * =peer_transports_cow; for (int i =0; i <peer_transports.size(); i++) { PEPeerTransport pt =(PEPeerTransport) peer_transports.get(i); if
     * (pt.getPeerItemIdentity().equals(peerItem)) return pt; } return null; }
     */

    public PEPeerTransport getTransportFromAddress(String peer) {
        final ArrayList peer_transports = peer_transports_cow;
        for (int i = 0; i < peer_transports.size(); i++) {
            final PEPeerTransport pt = (PEPeerTransport) peer_transports.get(i);
            if (peer.equals(pt.getIp()))
                return pt;
        }
        return null;
    }

    // Snubbed peers accounting
    public void incNbPeersSnubbed() {
        nbPeersSnubbed++;
    }

    public void decNbPeersSnubbed() {
        nbPeersSnubbed--;
    }

    public void setNbPeersSnubbed(int n) {
        nbPeersSnubbed = n;
    }

    public int getNbPeersSnubbed() {
        return nbPeersSnubbed;
    }

    public boolean getPreferUDP() {
        return (prefer_udp);
    }

    public void setPreferUDP(boolean prefer) {
        prefer_udp = prefer;
    }

    public boolean isPeerSourceEnabled(String peer_source) {
        return (adapter.isPeerSourceEnabled(peer_source));
    }

    public boolean isNetworkEnabled(String net) {
        return (adapter.isNetworkEnabled(net));
    }

    public void peerDiscovered(PEPeerTransport finder, PeerItem pi) {
        final ArrayList peer_manager_listeners = peer_manager_listeners_cow;

        for (int i = 0; i < peer_manager_listeners.size(); i++) {
            try {
                ((PEPeerManagerListener) peer_manager_listeners.get(i)).peerDiscovered(this, pi, finder);

            } catch (Throwable e) {

                Debug.printStackTrace(e);
            }
        }
    }

    public TrackerPeerSource getTrackerPeerSource() {
        return (new TrackerPeerSourceAdapter() {
            public int getType() {
                return (TP_PEX);
            }

            public int getStatus() {
                return (isPeerExchangeEnabled() ? ST_ONLINE : ST_DISABLED);
            }

            public String getName() {
                return (MessageText.getString("tps.pex.details", new String[] { String.valueOf(peer_transports_cow.size()),
                        String.valueOf(peer_database.getExchangedPeerCount()), String.valueOf(peer_database.getDiscoveredPeerCount()) }));
            }

            public int getPeers() {
                return (isPeerExchangeEnabled() ? peer_database.getExchangedPeersUsed() : -1);
            }
        });
    }

    public void generateEvidence(IndentWriter writer) {
        writer.println("PeerManager: seeding=" + seeding_mode);

        writer.println("    udp_fb=" + pending_nat_traversals.size() + ",udp_tc=" + udp_traversal_count + ",pd=[" + peer_database.getString() + "]");

        String pending_udp = "";

        try {
            peer_transports_mon.enter();

            Iterator it = pending_nat_traversals.values().iterator();

            while (it.hasNext()) {

                PEPeerTransport peer = (PEPeerTransport) it.next();

                pending_udp +=
                        (pending_udp.length() == 0 ? "" : ",") + peer.getPeerItemIdentity().getAddressString() + ":"
                                + peer.getPeerItemIdentity().getUDPPort();
            }
        } finally {

            peer_transports_mon.exit();
        }

        if (pending_udp.length() > 0) {

            writer.println("    pending_udp=" + pending_udp);
        }

        List traversals = PeerNATTraverser.getSingleton().getTraversals(this);

        String active_udp = "";

        Iterator it = traversals.iterator();

        while (it.hasNext()) {

            InetSocketAddress ad = (InetSocketAddress) it.next();

            active_udp += (active_udp.length() == 0 ? "" : ",") + AddressUtils.getHostAddress(ad) + ":" + ad.getPort();
        }

        if (active_udp.length() > 0) {

            writer.println("    active_udp=" + active_udp);
        }

        if (!seeding_mode) {

            writer.println("  Active Pieces");

            int num_active = 0;

            try {
                writer.indent();

                String str = "";
                int num = 0;

                for (int i = 0; i < pePieces.length; i++) {

                    PEPiece piece = pePieces[i];

                    if (piece != null) {

                        num_active++;

                        str += (str.length() == 0 ? "" : ",") + "#" + i + " " + dm_pieces[i].getString() + ": " + piece.getString();

                        num++;

                        if (num == 20) {

                            writer.println(str);
                            str = "";
                            num = 0;
                        }
                    }
                }

                if (num > 0) {
                    writer.println(str);
                }

            } finally {

                writer.exdent();
            }

            if (num_active == 0) {

                writer.println("  Inactive Pieces (excluding done/skipped)");

                try {
                    writer.indent();

                    String str = "";
                    int num = 0;

                    for (int i = 0; i < dm_pieces.length; i++) {

                        DiskManagerPiece dm_piece = dm_pieces[i];

                        if (dm_piece.isInteresting()) {

                            str += (str.length() == 0 ? "" : ",") + "#" + i + " " + dm_pieces[i].getString();

                            num++;

                            if (num == 20) {

                                writer.println(str);
                                str = "";
                                num = 0;
                            }
                        }
                    }

                    if (num > 0) {

                        writer.println(str);
                    }

                } finally {

                    writer.exdent();
                }
            }

            piecePicker.generateEvidence(writer);
        }

        try {
            peer_transports_mon.enter();

            writer.println("Peers: total = " + peer_transports_cow.size());

            writer.indent();

            try {
                writer.indent();

                it = peer_transports_cow.iterator();

                while (it.hasNext()) {

                    PEPeerTransport peer = (PEPeerTransport) it.next();

                    peer.generateEvidence(writer);
                }
            } finally {

                writer.exdent();
            }
        } finally {

            peer_transports_mon.exit();

            writer.exdent();
        }

        disk_mgr.generateEvidence(writer);
    }
}
